regscale-cli 6.21.1.0__py3-none-any.whl → 6.21.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- regscale/_version.py +1 -1
- regscale/core/app/application.py +7 -0
- regscale/integrations/commercial/__init__.py +8 -8
- regscale/integrations/commercial/import_all/import_all_cmd.py +2 -2
- regscale/integrations/commercial/microsoft_defender/__init__.py +0 -0
- regscale/integrations/commercial/{defender.py → microsoft_defender/defender.py} +38 -612
- regscale/integrations/commercial/microsoft_defender/defender_api.py +286 -0
- regscale/integrations/commercial/microsoft_defender/defender_constants.py +80 -0
- regscale/integrations/commercial/microsoft_defender/defender_scanner.py +168 -0
- regscale/integrations/commercial/qualys/__init__.py +24 -86
- regscale/integrations/commercial/qualys/containers.py +2 -0
- regscale/integrations/commercial/qualys/scanner.py +7 -2
- regscale/integrations/commercial/sonarcloud.py +110 -71
- regscale/integrations/commercial/wizv2/click.py +4 -1
- regscale/integrations/commercial/wizv2/data_fetcher.py +401 -0
- regscale/integrations/commercial/wizv2/finding_processor.py +295 -0
- regscale/integrations/commercial/wizv2/policy_compliance.py +1402 -203
- regscale/integrations/commercial/wizv2/policy_compliance_helpers.py +564 -0
- regscale/integrations/commercial/wizv2/scanner.py +4 -4
- regscale/integrations/compliance_integration.py +212 -60
- regscale/integrations/public/fedramp/fedramp_five.py +92 -7
- regscale/integrations/scanner_integration.py +27 -4
- regscale/models/__init__.py +1 -1
- regscale/models/integration_models/cisa_kev_data.json +33 -3
- regscale/models/integration_models/synqly_models/capabilities.json +1 -1
- regscale/models/regscale_models/issue.py +29 -9
- {regscale_cli-6.21.1.0.dist-info → regscale_cli-6.21.2.0.dist-info}/METADATA +1 -1
- {regscale_cli-6.21.1.0.dist-info → regscale_cli-6.21.2.0.dist-info}/RECORD +32 -27
- tests/regscale/test_authorization.py +0 -65
- tests/regscale/test_init.py +0 -96
- {regscale_cli-6.21.1.0.dist-info → regscale_cli-6.21.2.0.dist-info}/LICENSE +0 -0
- {regscale_cli-6.21.1.0.dist-info → regscale_cli-6.21.2.0.dist-info}/WHEEL +0 -0
- {regscale_cli-6.21.1.0.dist-info → regscale_cli-6.21.2.0.dist-info}/entry_points.txt +0 -0
- {regscale_cli-6.21.1.0.dist-info → regscale_cli-6.21.2.0.dist-info}/top_level.txt +0 -0
|
@@ -9,8 +9,8 @@ import re
|
|
|
9
9
|
from datetime import datetime
|
|
10
10
|
from typing import Dict, List, Optional, Iterator, Any
|
|
11
11
|
|
|
12
|
-
from regscale.core.app.utils.app_utils import error_and_exit, check_license
|
|
13
12
|
from regscale.core.app.application import Application
|
|
13
|
+
from regscale.core.app.utils.app_utils import error_and_exit, check_license, get_current_datetime
|
|
14
14
|
from regscale.integrations.commercial.wizv2.async_client import run_async_queries
|
|
15
15
|
from regscale.integrations.commercial.wizv2.constants import (
|
|
16
16
|
WizVulnerabilityType,
|
|
@@ -20,23 +20,36 @@ from regscale.integrations.commercial.wizv2.constants import (
|
|
|
20
20
|
FRAMEWORK_SHORTCUTS,
|
|
21
21
|
FRAMEWORK_CATEGORIES,
|
|
22
22
|
)
|
|
23
|
+
from regscale.integrations.commercial.wizv2.data_fetcher import PolicyAssessmentFetcher
|
|
24
|
+
from regscale.integrations.commercial.wizv2.finding_processor import (
|
|
25
|
+
FindingConsolidator,
|
|
26
|
+
FindingToIssueProcessor,
|
|
27
|
+
)
|
|
28
|
+
from regscale.integrations.commercial.wizv2.policy_compliance_helpers import (
|
|
29
|
+
ControlImplementationCache,
|
|
30
|
+
AssetConsolidator,
|
|
31
|
+
IssueFieldSetter,
|
|
32
|
+
ControlAssessmentProcessor,
|
|
33
|
+
)
|
|
23
34
|
from regscale.integrations.commercial.wizv2.wiz_auth import wiz_authenticate
|
|
24
35
|
from regscale.integrations.compliance_integration import ComplianceIntegration, ComplianceItem
|
|
25
36
|
from regscale.integrations.scanner_integration import (
|
|
26
37
|
ScannerIntegrationType,
|
|
27
38
|
IntegrationAsset,
|
|
28
39
|
IntegrationFinding,
|
|
40
|
+
issue_due_date,
|
|
29
41
|
)
|
|
30
42
|
from regscale.models import regscale_models
|
|
31
43
|
|
|
32
44
|
logger = logging.getLogger("regscale")
|
|
33
45
|
|
|
34
46
|
|
|
47
|
+
# Constants for file operations
|
|
35
48
|
JSON_FILE_EXT = ".json"
|
|
36
49
|
JSONL_FILE_EXT = ".jsonl"
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
50
|
+
MAX_DISPLAY_ASSETS = 10 # Maximum number of asset names to display in descriptions
|
|
51
|
+
CACHE_CLEANUP_KEEP_COUNT = 5 # Number of recent cache files to keep during cleanup
|
|
52
|
+
WIZ_URL = "https://api.wiz.io/graphql"
|
|
40
53
|
|
|
41
54
|
# Safer, linear-time regex for control-id normalization.
|
|
42
55
|
# Examples supported: 'AC-4', 'AC-4(2)', 'AC-4 (2)', 'AC-4-2', 'AC-4 2'
|
|
@@ -83,8 +96,8 @@ class WizComplianceItem(ComplianceItem):
|
|
|
83
96
|
filtered = [
|
|
84
97
|
sc for sc in subcategories if sc.get("category", {}).get("framework", {}).get("id") == target_framework_id
|
|
85
98
|
]
|
|
86
|
-
#
|
|
87
|
-
return filtered
|
|
99
|
+
# Return filtered results - if empty, the control_id will be empty (framework filtering working as intended)
|
|
100
|
+
return filtered
|
|
88
101
|
|
|
89
102
|
@property
|
|
90
103
|
def resource_id(self) -> str:
|
|
@@ -178,12 +191,15 @@ class WizPolicyComplianceIntegration(ComplianceIntegration):
|
|
|
178
191
|
|
|
179
192
|
title = "Wiz Policy Compliance Integration"
|
|
180
193
|
type = ScannerIntegrationType.CONTROL_TEST
|
|
181
|
-
#
|
|
182
|
-
|
|
194
|
+
# Use wizId field for asset identification (matches other Wiz integrations)
|
|
195
|
+
asset_identifier_field = "wizId"
|
|
196
|
+
# Do not create assets - they come from separate inventory import
|
|
197
|
+
options_map_assets_to_components: bool = False
|
|
183
198
|
# Do not create vulnerabilities from compliance policy results
|
|
184
199
|
create_vulnerabilities: bool = False
|
|
185
|
-
#
|
|
186
|
-
enable_scan_history: bool =
|
|
200
|
+
# Do not create scan history - this is compliance report ingest, not a vulnerability scan
|
|
201
|
+
enable_scan_history: bool = False
|
|
202
|
+
|
|
187
203
|
# Control whether JSONL control-centric export is written alongside JSON
|
|
188
204
|
write_jsonl_output: bool = False
|
|
189
205
|
|
|
@@ -199,6 +215,7 @@ class WizPolicyComplianceIntegration(ComplianceIntegration):
|
|
|
199
215
|
create_issues: bool = True,
|
|
200
216
|
update_control_status: bool = True,
|
|
201
217
|
create_poams: bool = False,
|
|
218
|
+
regscale_module: Optional[str] = "securityplans",
|
|
202
219
|
**kwargs,
|
|
203
220
|
):
|
|
204
221
|
"""
|
|
@@ -214,9 +231,11 @@ class WizPolicyComplianceIntegration(ComplianceIntegration):
|
|
|
214
231
|
:param bool create_issues: Whether to create issues for failed compliance
|
|
215
232
|
:param bool update_control_status: Whether to update control implementation status
|
|
216
233
|
:param bool create_poams: Whether to mark issues as POAMs
|
|
234
|
+
:param Optional[str] regscale_module: RegScale module string (overrides default parent_module)
|
|
217
235
|
"""
|
|
218
236
|
super().__init__(
|
|
219
237
|
plan_id=plan_id,
|
|
238
|
+
parent_module=regscale_module,
|
|
220
239
|
catalog_id=catalog_id,
|
|
221
240
|
framework=self._map_framework_id_to_name(framework_id),
|
|
222
241
|
create_issues=create_issues,
|
|
@@ -226,6 +245,10 @@ class WizPolicyComplianceIntegration(ComplianceIntegration):
|
|
|
226
245
|
**kwargs,
|
|
227
246
|
)
|
|
228
247
|
|
|
248
|
+
# Override parent_module if regscale_module is provided
|
|
249
|
+
if regscale_module:
|
|
250
|
+
self.parent_module = regscale_module
|
|
251
|
+
|
|
229
252
|
self.wiz_project_id = wiz_project_id
|
|
230
253
|
self.client_id = client_id
|
|
231
254
|
self.client_secret = client_secret
|
|
@@ -245,21 +268,87 @@ class WizPolicyComplianceIntegration(ComplianceIntegration):
|
|
|
245
268
|
self.policy_cache_dir, f"policy_assessments_{wiz_project_id}_{framework_id}.json"
|
|
246
269
|
)
|
|
247
270
|
|
|
271
|
+
# Initialize helper classes for cleaner code organization
|
|
272
|
+
self._control_cache = ControlImplementationCache()
|
|
273
|
+
self._asset_consolidator = AssetConsolidator()
|
|
274
|
+
self._issue_field_setter = IssueFieldSetter(self._control_cache, plan_id, regscale_module or "securityplans")
|
|
275
|
+
self._finding_consolidator = FindingConsolidator(self)
|
|
276
|
+
self._finding_processor = FindingToIssueProcessor(self)
|
|
277
|
+
self._assessment_processor = ControlAssessmentProcessor(
|
|
278
|
+
plan_id,
|
|
279
|
+
regscale_module or "securityplans",
|
|
280
|
+
self.scan_date,
|
|
281
|
+
self.title,
|
|
282
|
+
self._map_framework_id_to_name(framework_id),
|
|
283
|
+
)
|
|
284
|
+
|
|
248
285
|
def fetch_compliance_data(self) -> List[Any]:
|
|
249
286
|
"""
|
|
250
|
-
Fetch compliance data from Wiz GraphQL API
|
|
287
|
+
Fetch compliance data from Wiz GraphQL API and filter to framework-specific
|
|
288
|
+
items for existing assets only.
|
|
251
289
|
|
|
252
|
-
:return: List of raw compliance data
|
|
290
|
+
:return: List of filtered raw compliance data
|
|
253
291
|
:rtype: List[Any]
|
|
254
292
|
"""
|
|
255
293
|
# Authenticate if not already done
|
|
256
294
|
if not self.access_token:
|
|
257
295
|
self.authenticate_wiz()
|
|
258
296
|
|
|
259
|
-
#
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
297
|
+
# Load existing assets early for filtering
|
|
298
|
+
self._load_regscale_assets()
|
|
299
|
+
|
|
300
|
+
# Use the data fetcher for cleaner code
|
|
301
|
+
fetcher = PolicyAssessmentFetcher(
|
|
302
|
+
wiz_endpoint=self.wiz_endpoint or WIZ_URL,
|
|
303
|
+
access_token=self.access_token,
|
|
304
|
+
wiz_project_id=self.wiz_project_id,
|
|
305
|
+
framework_id=self.framework_id,
|
|
306
|
+
cache_duration_minutes=self.cache_duration_minutes,
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
all_policy_assessments = fetcher.fetch_policy_assessments()
|
|
310
|
+
|
|
311
|
+
if not all_policy_assessments:
|
|
312
|
+
logger.info("No policy assessments fetched from Wiz")
|
|
313
|
+
self.raw_policy_assessments = []
|
|
314
|
+
return []
|
|
315
|
+
|
|
316
|
+
# Filter to only items with existing assets in RegScale
|
|
317
|
+
filtered_assessments = self._filter_assessments_to_existing_assets(all_policy_assessments)
|
|
318
|
+
|
|
319
|
+
self.raw_policy_assessments = filtered_assessments
|
|
320
|
+
return filtered_assessments
|
|
321
|
+
|
|
322
|
+
def _filter_assessments_to_existing_assets(self, assessments: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
323
|
+
"""
|
|
324
|
+
Filter assessments to only include items with existing assets and control IDs.
|
|
325
|
+
|
|
326
|
+
:param assessments: List of raw assessments from Wiz
|
|
327
|
+
:return: Filtered list of assessments
|
|
328
|
+
"""
|
|
329
|
+
assets_exist = getattr(self, "_regscale_assets_by_wiz_id", {})
|
|
330
|
+
filtered_assessments = []
|
|
331
|
+
skipped_no_control = 0
|
|
332
|
+
skipped_no_asset = 0
|
|
333
|
+
|
|
334
|
+
for assessment in assessments:
|
|
335
|
+
# Convert to compliance item to check framework and asset existence
|
|
336
|
+
temp_item = WizComplianceItem(assessment, self)
|
|
337
|
+
|
|
338
|
+
# Skip if no control ID (not in selected framework)
|
|
339
|
+
if not temp_item.control_id:
|
|
340
|
+
skipped_no_control += 1
|
|
341
|
+
continue
|
|
342
|
+
|
|
343
|
+
# Skip if asset doesn't exist in RegScale (use cached lookup)
|
|
344
|
+
if temp_item.resource_id not in assets_exist:
|
|
345
|
+
skipped_no_asset += 1
|
|
346
|
+
continue
|
|
347
|
+
|
|
348
|
+
filtered_assessments.append(assessment)
|
|
349
|
+
logger.debug(f"Skipped {skipped_no_control} assessments with no control ID for framework.")
|
|
350
|
+
logger.debug(f"Skipped {skipped_no_asset} assessments with no existing asset in RegScale.")
|
|
351
|
+
return filtered_assessments
|
|
263
352
|
|
|
264
353
|
def create_compliance_item(self, raw_data: Any) -> ComplianceItem:
|
|
265
354
|
"""
|
|
@@ -309,66 +398,268 @@ class WizPolicyComplianceIntegration(ComplianceIntegration):
|
|
|
309
398
|
|
|
310
399
|
def fetch_assets(self, *args, **kwargs) -> Iterator[IntegrationAsset]:
|
|
311
400
|
"""
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
401
|
+
No assets are created in policy compliance integration.
|
|
402
|
+
Assets come from separate Wiz inventory import.
|
|
403
|
+
"""
|
|
404
|
+
return iter([])
|
|
405
|
+
|
|
406
|
+
def fetch_findings(self, *args, **kwargs) -> Iterator[IntegrationFinding]:
|
|
407
|
+
"""
|
|
408
|
+
Create consolidated findings grouped by control, with all affected resources under each control.
|
|
315
409
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
- Always yield unique assets for bulk upsert (create or update)
|
|
410
|
+
This approach groups by control first, then collects all resources that fail that control.
|
|
411
|
+
This results in one finding per control with multiple resources, making consolidation much easier.
|
|
319
412
|
"""
|
|
320
|
-
|
|
413
|
+
if not self.failed_compliance_items:
|
|
414
|
+
return
|
|
321
415
|
|
|
322
|
-
#
|
|
323
|
-
self.
|
|
416
|
+
# Use the finding consolidator for cleaner code
|
|
417
|
+
yield from self._finding_consolidator.create_consolidated_findings(self.failed_compliance_items)
|
|
324
418
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
if not resource_id or resource_id in processed_resources:
|
|
329
|
-
continue
|
|
419
|
+
def _get_all_control_ids_for_compliance_item(self, compliance_item: WizComplianceItem) -> List[str]:
|
|
420
|
+
"""
|
|
421
|
+
Get ALL control IDs that a compliance item maps to.
|
|
330
422
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
# Derive component grouping from the source asset type (not control)
|
|
334
|
-
component_name = self._get_component_name_from_source_type(compliance_item)
|
|
335
|
-
if isinstance(getattr(asset, "component_names", None), list) and component_name:
|
|
336
|
-
if component_name not in asset.component_names:
|
|
337
|
-
asset.component_names.append(component_name)
|
|
423
|
+
Wiz policies can map to multiple controls (e.g., one policy failure might affect
|
|
424
|
+
AC-4(2), AC-4(4), and SC-28(1) controls). This method returns all of them.
|
|
338
425
|
|
|
339
|
-
|
|
340
|
-
|
|
426
|
+
:param WizComplianceItem compliance_item: Compliance item to extract control IDs from
|
|
427
|
+
:return: List of control IDs this policy maps to
|
|
428
|
+
:rtype: List[str]
|
|
429
|
+
"""
|
|
430
|
+
if not compliance_item.policy:
|
|
431
|
+
return []
|
|
341
432
|
|
|
342
|
-
|
|
433
|
+
subcategories = compliance_item._get_filtered_subcategories()
|
|
434
|
+
if not subcategories:
|
|
435
|
+
return []
|
|
436
|
+
|
|
437
|
+
# Extract control IDs and deduplicate in one pass
|
|
438
|
+
unique_control_ids = []
|
|
439
|
+
seen = set()
|
|
440
|
+
|
|
441
|
+
for subcat in subcategories:
|
|
442
|
+
external_id = subcat.get("externalId", "")
|
|
443
|
+
if external_id and external_id not in seen:
|
|
444
|
+
seen.add(external_id)
|
|
445
|
+
unique_control_ids.append(external_id)
|
|
446
|
+
|
|
447
|
+
return unique_control_ids
|
|
448
|
+
|
|
449
|
+
def _group_compliance_items_by_control(self) -> Dict[str, Dict[str, WizComplianceItem]]:
|
|
343
450
|
"""
|
|
344
|
-
|
|
451
|
+
Group failed compliance items by control ID.
|
|
345
452
|
|
|
346
|
-
|
|
453
|
+
:return: Dictionary mapping control IDs to resource dictionaries
|
|
454
|
+
:rtype: Dict[str, Dict[str, WizComplianceItem]]
|
|
347
455
|
"""
|
|
348
|
-
|
|
456
|
+
control_to_resources = {} # {control_id: {resource_id: compliance_item}}
|
|
349
457
|
|
|
350
|
-
seen_keys: set[tuple[str, str]] = set()
|
|
351
458
|
for compliance_item in self.failed_compliance_items:
|
|
352
459
|
if not isinstance(compliance_item, WizComplianceItem):
|
|
353
|
-
finding = super().create_finding_from_compliance_item(compliance_item)
|
|
354
|
-
if finding:
|
|
355
|
-
yield finding
|
|
356
460
|
continue
|
|
357
461
|
|
|
358
462
|
asset_id = (compliance_item.resource_id or "").lower()
|
|
359
|
-
|
|
360
|
-
if not asset_id or not control:
|
|
463
|
+
if not asset_id:
|
|
361
464
|
continue
|
|
362
465
|
|
|
363
|
-
|
|
364
|
-
|
|
466
|
+
# Get ALL control IDs that this policy assessment maps to
|
|
467
|
+
all_control_ids = self._get_all_control_ids_for_compliance_item(compliance_item)
|
|
468
|
+
if not all_control_ids:
|
|
365
469
|
continue
|
|
366
|
-
seen_keys.add(key)
|
|
367
470
|
|
|
368
|
-
|
|
471
|
+
# Add this resource to each control it fails
|
|
472
|
+
for control_id in all_control_ids:
|
|
473
|
+
control = control_id.upper()
|
|
474
|
+
|
|
475
|
+
if control not in control_to_resources:
|
|
476
|
+
control_to_resources[control] = {}
|
|
477
|
+
|
|
478
|
+
# Use the first compliance item we find for this resource-control pair
|
|
479
|
+
# (there might be duplicates from multiple policy assessments)
|
|
480
|
+
if asset_id not in control_to_resources[control]:
|
|
481
|
+
control_to_resources[control][asset_id] = compliance_item
|
|
482
|
+
|
|
483
|
+
return control_to_resources
|
|
484
|
+
|
|
485
|
+
def _create_consolidated_findings(
|
|
486
|
+
self, control_to_resources: Dict[str, Dict[str, WizComplianceItem]]
|
|
487
|
+
) -> Iterator[IntegrationFinding]:
|
|
488
|
+
"""
|
|
489
|
+
Create consolidated findings from grouped control-resource mappings.
|
|
490
|
+
|
|
491
|
+
:param Dict[str, Dict[str, WizComplianceItem]] control_to_resources: Control groupings
|
|
492
|
+
:yield: Consolidated findings
|
|
493
|
+
:rtype: Iterator[IntegrationFinding]
|
|
494
|
+
"""
|
|
495
|
+
for control_id, resources in control_to_resources.items():
|
|
496
|
+
|
|
497
|
+
# Use the first compliance item as the base for this control's finding
|
|
498
|
+
base_compliance_item = next(iter(resources.values()))
|
|
499
|
+
|
|
500
|
+
# Create a consolidated finding for this control
|
|
501
|
+
finding = self._create_consolidated_finding_for_control(
|
|
502
|
+
control_id=control_id, compliance_item=base_compliance_item, affected_resources=list(resources.keys())
|
|
503
|
+
)
|
|
504
|
+
|
|
369
505
|
if finding:
|
|
370
506
|
yield finding
|
|
371
507
|
|
|
508
|
+
def _create_consolidated_finding_for_control(
|
|
509
|
+
self, control_id: str, compliance_item: WizComplianceItem, affected_resources: List[str]
|
|
510
|
+
) -> Optional[IntegrationFinding]:
|
|
511
|
+
"""
|
|
512
|
+
Create a consolidated finding for a control with all affected resources.
|
|
513
|
+
|
|
514
|
+
:param str control_id: The control ID (e.g., 'AC-4(2)')
|
|
515
|
+
:param WizComplianceItem compliance_item: Base compliance item for this control
|
|
516
|
+
:param List[str] affected_resources: List of Wiz resource IDs that fail this control
|
|
517
|
+
:return: Consolidated finding with all affected resources
|
|
518
|
+
:rtype: Optional[IntegrationFinding]
|
|
519
|
+
"""
|
|
520
|
+
# Filter to only resources that exist as assets in RegScale
|
|
521
|
+
asset_mappings = self._build_asset_mappings(affected_resources)
|
|
522
|
+
|
|
523
|
+
if not asset_mappings:
|
|
524
|
+
return None
|
|
525
|
+
|
|
526
|
+
# Create the base finding using the control-specific approach
|
|
527
|
+
finding = self._create_finding_for_specific_control(compliance_item, control_id)
|
|
528
|
+
if not finding:
|
|
529
|
+
return None
|
|
530
|
+
|
|
531
|
+
# Update the asset identifier and description with consolidated info
|
|
532
|
+
self._update_finding_with_consolidated_assets(finding, asset_mappings)
|
|
533
|
+
return finding
|
|
534
|
+
|
|
535
|
+
def _build_asset_mappings(self, resource_ids: List[str]) -> Dict[str, Dict[str, str]]:
|
|
536
|
+
"""
|
|
537
|
+
Build asset mappings for resources that exist in RegScale.
|
|
538
|
+
|
|
539
|
+
:param List[str] resource_ids: List of Wiz resource IDs
|
|
540
|
+
:return: Mapping of resource IDs to asset information
|
|
541
|
+
:rtype: Dict[str, Dict[str, str]]
|
|
542
|
+
"""
|
|
543
|
+
asset_mappings = {}
|
|
544
|
+
|
|
545
|
+
for resource_id in resource_ids:
|
|
546
|
+
if self._asset_exists_in_regscale(resource_id):
|
|
547
|
+
asset = self.get_asset_by_identifier(resource_id)
|
|
548
|
+
if asset and asset.name:
|
|
549
|
+
asset_mappings[resource_id] = {"name": asset.name, "wiz_id": resource_id}
|
|
550
|
+
else:
|
|
551
|
+
# Fallback to resource ID if asset name not found
|
|
552
|
+
asset_mappings[resource_id] = {"name": resource_id, "wiz_id": resource_id}
|
|
553
|
+
|
|
554
|
+
return asset_mappings
|
|
555
|
+
|
|
556
|
+
def _update_finding_with_consolidated_assets(
|
|
557
|
+
self, finding: IntegrationFinding, asset_mappings: Dict[str, Dict[str, str]]
|
|
558
|
+
) -> None:
|
|
559
|
+
"""
|
|
560
|
+
Update a finding with consolidated asset information.
|
|
561
|
+
|
|
562
|
+
:param IntegrationFinding finding: Finding to update
|
|
563
|
+
:param Dict[str, Dict[str, str]] asset_mappings: Asset mapping information
|
|
564
|
+
:return: None
|
|
565
|
+
:rtype: None
|
|
566
|
+
"""
|
|
567
|
+
# Update the asset identifier to include all asset names (clean format for POAMs)
|
|
568
|
+
consolidated_asset_identifier = self._create_consolidated_asset_identifier(asset_mappings)
|
|
569
|
+
finding.asset_identifier = consolidated_asset_identifier
|
|
570
|
+
|
|
571
|
+
# Update finding description to indicate multiple resources
|
|
572
|
+
asset_names = [info["name"] for info in asset_mappings.values()]
|
|
573
|
+
if len(asset_names) > 1:
|
|
574
|
+
finding.description = f"{finding.description}\n\nThis control failure affects {len(asset_names)} assets: {', '.join(asset_names[:MAX_DISPLAY_ASSETS])}"
|
|
575
|
+
if len(asset_names) > MAX_DISPLAY_ASSETS:
|
|
576
|
+
finding.description += f" (and {len(asset_names) - MAX_DISPLAY_ASSETS} more)"
|
|
577
|
+
|
|
578
|
+
def _create_finding_for_specific_control(
|
|
579
|
+
self, compliance_item: WizComplianceItem, control_id: str
|
|
580
|
+
) -> Optional[IntegrationFinding]:
|
|
581
|
+
"""
|
|
582
|
+
Create a finding for a specific control ID from a compliance item.
|
|
583
|
+
|
|
584
|
+
This is similar to create_finding_from_compliance_item but ensures the finding
|
|
585
|
+
uses the specific control ID rather than just the first one.
|
|
586
|
+
|
|
587
|
+
:param WizComplianceItem compliance_item: Source compliance item
|
|
588
|
+
:param str control_id: Specific control ID to create finding for
|
|
589
|
+
:return: Integration finding for this specific control
|
|
590
|
+
:rtype: Optional[IntegrationFinding]
|
|
591
|
+
"""
|
|
592
|
+
try:
|
|
593
|
+
control_labels = [control_id] if control_id else []
|
|
594
|
+
severity = self._map_severity(compliance_item.severity)
|
|
595
|
+
policy_name = self._get_policy_name(compliance_item)
|
|
596
|
+
title = f"{policy_name} ({control_id})" if control_id else policy_name
|
|
597
|
+
description = self._compose_description(policy_name, compliance_item)
|
|
598
|
+
|
|
599
|
+
finding = self._build_finding(
|
|
600
|
+
control_labels=control_labels,
|
|
601
|
+
title=title,
|
|
602
|
+
description=description,
|
|
603
|
+
severity=severity,
|
|
604
|
+
compliance_item=compliance_item,
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
# Set the specific control ID for this finding
|
|
608
|
+
finding.rule_id = control_id
|
|
609
|
+
finding.affected_controls = self._normalize_control_id_string(control_id)
|
|
610
|
+
|
|
611
|
+
# Ensure unique external_id for each control to prevent unwanted updates
|
|
612
|
+
finding.external_id = f"wiz-policy-control-{control_id.upper()}-{self.framework_id}"
|
|
613
|
+
|
|
614
|
+
self._set_assessment_id_if_available(finding, compliance_item)
|
|
615
|
+
return finding
|
|
616
|
+
|
|
617
|
+
except Exception as e:
|
|
618
|
+
logger.error(f"Error creating finding for control {control_id}: {e}")
|
|
619
|
+
return None
|
|
620
|
+
|
|
621
|
+
def _asset_exists_in_regscale(self, resource_id: str) -> bool:
|
|
622
|
+
"""
|
|
623
|
+
Check if an asset with the given Wiz resource ID exists in RegScale.
|
|
624
|
+
|
|
625
|
+
:param str resource_id: Wiz resource ID to check (stored in RegScale asset wizId field)
|
|
626
|
+
:return: True if asset exists, False otherwise
|
|
627
|
+
:rtype: bool
|
|
628
|
+
"""
|
|
629
|
+
if not resource_id:
|
|
630
|
+
return False
|
|
631
|
+
|
|
632
|
+
try:
|
|
633
|
+
# Check if we have a cached lookup of existing assets
|
|
634
|
+
if not hasattr(self, "_regscale_assets_by_wiz_id"):
|
|
635
|
+
self._load_regscale_assets()
|
|
636
|
+
|
|
637
|
+
return resource_id in self._regscale_assets_by_wiz_id
|
|
638
|
+
except Exception:
|
|
639
|
+
return False
|
|
640
|
+
|
|
641
|
+
def _load_regscale_assets(self) -> None:
|
|
642
|
+
"""
|
|
643
|
+
Load all existing assets from RegScale into a Wiz ID-based lookup cache.
|
|
644
|
+
Wiz resource IDs are stored in the RegScale asset wizId field.
|
|
645
|
+
"""
|
|
646
|
+
try:
|
|
647
|
+
logger.info("Loading existing assets from RegScale for asset existence checks...")
|
|
648
|
+
# Get all assets for the current plan
|
|
649
|
+
existing_assets = regscale_models.Asset.get_all_by_parent(
|
|
650
|
+
parent_id=self.plan_id,
|
|
651
|
+
parent_module=self.parent_module,
|
|
652
|
+
)
|
|
653
|
+
|
|
654
|
+
# Create Wiz ID-based lookup cache (Wiz resource ID -> RegScale asset)
|
|
655
|
+
self._regscale_assets_by_wiz_id = {asset.wizId: asset for asset in existing_assets if asset.wizId}
|
|
656
|
+
logger.info(f"Loaded {len(self._regscale_assets_by_wiz_id)} existing assets for lookup")
|
|
657
|
+
|
|
658
|
+
except Exception as e:
|
|
659
|
+
logger.error(f"Error loading RegScale assets: {e}")
|
|
660
|
+
# Initialize empty cache to avoid repeated failures
|
|
661
|
+
self._regscale_assets_by_wiz_id = {}
|
|
662
|
+
|
|
372
663
|
def _map_framework_id_to_name(self, framework_id: str) -> str:
|
|
373
664
|
"""
|
|
374
665
|
Map framework ID to framework name.
|
|
@@ -531,7 +822,7 @@ class WizPolicyComplianceIntegration(ComplianceIntegration):
|
|
|
531
822
|
first_seen=self.scan_date,
|
|
532
823
|
last_seen=self.scan_date,
|
|
533
824
|
scan_date=self.scan_date,
|
|
534
|
-
asset_identifier=compliance_item
|
|
825
|
+
asset_identifier=self._get_regscale_asset_identifier(compliance_item),
|
|
535
826
|
vulnerability_type="Policy Compliance Violation",
|
|
536
827
|
rule_id=compliance_item.control_id,
|
|
537
828
|
baseline=compliance_item.framework,
|
|
@@ -567,9 +858,8 @@ class WizPolicyComplianceIntegration(ComplianceIntegration):
|
|
|
567
858
|
assess = self._assessment_by_impl_today.get(impl_id)
|
|
568
859
|
if assess:
|
|
569
860
|
finding.assessment_id = assess.id
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
logger.debug(f"Error setting finding assessment ID: {e}")
|
|
861
|
+
except Exception:
|
|
862
|
+
pass
|
|
573
863
|
|
|
574
864
|
def create_asset_from_compliance_item(self, compliance_item: ComplianceItem) -> Optional[IntegrationAsset]:
|
|
575
865
|
"""
|
|
@@ -616,7 +906,7 @@ class WizPolicyComplianceIntegration(ComplianceIntegration):
|
|
|
616
906
|
|
|
617
907
|
asset = IntegrationAsset(
|
|
618
908
|
name=compliance_item.resource_name,
|
|
619
|
-
identifier=compliance_item.resource_id,
|
|
909
|
+
identifier=f"{compliance_item.resource_name} ({compliance_item.resource_id})",
|
|
620
910
|
external_id=compliance_item.resource_id,
|
|
621
911
|
other_tracking_number=compliance_item.resource_id, # For deduplication
|
|
622
912
|
asset_type=asset_type,
|
|
@@ -640,8 +930,8 @@ class WizPolicyComplianceIntegration(ComplianceIntegration):
|
|
|
640
930
|
return None
|
|
641
931
|
|
|
642
932
|
def create_scan_history(self): # type: ignore[override]
|
|
643
|
-
"""
|
|
644
|
-
return
|
|
933
|
+
"""No scan history created for compliance report ingest."""
|
|
934
|
+
return None
|
|
645
935
|
|
|
646
936
|
def _create_asset_notes(self, compliance_item: WizComplianceItem) -> str:
|
|
647
937
|
"""
|
|
@@ -741,12 +1031,11 @@ class WizPolicyComplianceIntegration(ComplianceIntegration):
|
|
|
741
1031
|
# Try cache first unless forced refresh
|
|
742
1032
|
cached_nodes = self._load_assessments_from_cache()
|
|
743
1033
|
if cached_nodes is not None:
|
|
744
|
-
logger.info(
|
|
1034
|
+
logger.info("Using cached Wiz policy assessments")
|
|
745
1035
|
return cached_nodes
|
|
746
1036
|
|
|
747
1037
|
# Only include variables supported by the query (avoid validation errors)
|
|
748
1038
|
page_size = 100
|
|
749
|
-
logger.info(f"Using Wiz policy assessments page size (first): {page_size}")
|
|
750
1039
|
base_variables = {"first": page_size}
|
|
751
1040
|
|
|
752
1041
|
# Try multiple filter key variants to avoid schema differences across tenants
|
|
@@ -768,7 +1057,7 @@ class WizPolicyComplianceIntegration(ComplianceIntegration):
|
|
|
768
1057
|
total=1,
|
|
769
1058
|
)
|
|
770
1059
|
results = run_async_queries(
|
|
771
|
-
endpoint=self.wiz_endpoint or
|
|
1060
|
+
endpoint=self.wiz_endpoint or WIZ_URL,
|
|
772
1061
|
headers=headers,
|
|
773
1062
|
query_configs=[
|
|
774
1063
|
{
|
|
@@ -863,7 +1152,7 @@ class WizPolicyComplianceIntegration(ComplianceIntegration):
|
|
|
863
1152
|
# If endpoint is not set (tests), short-circuit to async path mock
|
|
864
1153
|
if not self.wiz_endpoint:
|
|
865
1154
|
results = run_async_queries(
|
|
866
|
-
endpoint=
|
|
1155
|
+
endpoint=WIZ_URL,
|
|
867
1156
|
headers=headers,
|
|
868
1157
|
query_configs=[
|
|
869
1158
|
{
|
|
@@ -892,7 +1181,6 @@ class WizPolicyComplianceIntegration(ComplianceIntegration):
|
|
|
892
1181
|
)
|
|
893
1182
|
except Exception as exc: # noqa: BLE001 - propagate last error
|
|
894
1183
|
last_error = exc
|
|
895
|
-
logger.debug(f"Filter variant {fv} failed: {exc}")
|
|
896
1184
|
|
|
897
1185
|
msg = f"Failed to fetch policy assessments after trying all filter variants: {last_error}"
|
|
898
1186
|
logger.error(msg)
|
|
@@ -929,9 +1217,7 @@ class WizPolicyComplianceIntegration(ComplianceIntegration):
|
|
|
929
1217
|
variant_name = self._variant_name(filter_variant)
|
|
930
1218
|
progress.update(
|
|
931
1219
|
task,
|
|
932
|
-
description=(
|
|
933
|
-
f"[#f68d1f]Fetching Wiz policy assessments (limit: {page_size}, " f"variant: {variant_name})..."
|
|
934
|
-
),
|
|
1220
|
+
description=(f"[#f68d1f]Fetching Wiz policy assessments (limit: {page_size}, variant: {variant_name})..."),
|
|
935
1221
|
advance=1,
|
|
936
1222
|
)
|
|
937
1223
|
|
|
@@ -957,7 +1243,8 @@ class WizPolicyComplianceIntegration(ComplianceIntegration):
|
|
|
957
1243
|
completed=1,
|
|
958
1244
|
total=1,
|
|
959
1245
|
)
|
|
960
|
-
logger.info(
|
|
1246
|
+
logger.info("Successfully fetched Wiz policy assessments")
|
|
1247
|
+
|
|
961
1248
|
return filtered_nodes
|
|
962
1249
|
|
|
963
1250
|
def _execute_wiz_policy_query_paginated(
|
|
@@ -1158,12 +1445,10 @@ class WizPolicyComplianceIntegration(ComplianceIntegration):
|
|
|
1158
1445
|
for control_id, ctrl in control_agg.items():
|
|
1159
1446
|
jf.write(json.dumps(ctrl, ensure_ascii=False) + "\n")
|
|
1160
1447
|
logger.info(f"Policy compliance JSONL written to: {file_path_jsonl}")
|
|
1161
|
-
|
|
1162
|
-
self._cleanup_artifacts(artifacts_dir, keep=5)
|
|
1448
|
+
self._cleanup_artifacts(artifacts_dir, keep=CACHE_CLEANUP_KEEP_COUNT)
|
|
1163
1449
|
return file_path
|
|
1164
1450
|
|
|
1165
1451
|
except Exception as e:
|
|
1166
|
-
logger.error(f"Failed to write policy data to JSON: {str(e)}")
|
|
1167
1452
|
error_and_exit(f"Failed to write policy data to JSON: {str(e)}")
|
|
1168
1453
|
|
|
1169
1454
|
def _build_control_aggregation(self) -> Dict[str, Dict[str, Any]]:
|
|
@@ -1300,7 +1585,7 @@ class WizPolicyComplianceIntegration(ComplianceIntegration):
|
|
|
1300
1585
|
logger.error(f"Error parsing JSONL {jsonl_path}: {exc}")
|
|
1301
1586
|
return aggregated
|
|
1302
1587
|
|
|
1303
|
-
def _cleanup_artifacts(self, dir_path: str, keep: int =
|
|
1588
|
+
def _cleanup_artifacts(self, dir_path: str, keep: int = CACHE_CLEANUP_KEEP_COUNT) -> None:
|
|
1304
1589
|
"""
|
|
1305
1590
|
Keep the most recent JSON and JSONL policy_compliance_report files, delete older ones.
|
|
1306
1591
|
|
|
@@ -1330,8 +1615,8 @@ class WizPolicyComplianceIntegration(ComplianceIntegration):
|
|
|
1330
1615
|
except Exception:
|
|
1331
1616
|
# Non-fatal; continue cleanup
|
|
1332
1617
|
pass
|
|
1333
|
-
except Exception
|
|
1334
|
-
|
|
1618
|
+
except Exception:
|
|
1619
|
+
pass
|
|
1335
1620
|
|
|
1336
1621
|
def load_or_create_framework_mapping(self) -> Dict[str, str]:
|
|
1337
1622
|
"""
|
|
@@ -1439,7 +1724,6 @@ class WizPolicyComplianceIntegration(ComplianceIntegration):
|
|
|
1439
1724
|
return nodes
|
|
1440
1725
|
|
|
1441
1726
|
except Exception as e:
|
|
1442
|
-
logger.error(f"Failed to fetch security frameworks: {str(e)}")
|
|
1443
1727
|
error_and_exit(f"Failed to fetch security frameworks: {str(e)}")
|
|
1444
1728
|
|
|
1445
1729
|
def _create_framework_mapping(self, frameworks: List[Dict[str, Any]]) -> Dict[str, str]:
|
|
@@ -1515,6 +1799,45 @@ class WizPolicyComplianceIntegration(ComplianceIntegration):
|
|
|
1515
1799
|
|
|
1516
1800
|
return self.framework_mapping.get(framework_id, framework_id)
|
|
1517
1801
|
|
|
1802
|
+
def sync_compliance(self) -> None:
|
|
1803
|
+
"""
|
|
1804
|
+
Override base sync_compliance to ensure proper order for controlId/assessmentId assignment.
|
|
1805
|
+
|
|
1806
|
+
CRITICAL: Control assessments MUST be created BEFORE issues are processed
|
|
1807
|
+
to ensure controlId and assessmentId can be properly set.
|
|
1808
|
+
"""
|
|
1809
|
+
logger.info(f"Starting {self.title} compliance sync with proper assessment ordering...")
|
|
1810
|
+
|
|
1811
|
+
try:
|
|
1812
|
+
scan_history = self.create_scan_history()
|
|
1813
|
+
self.process_compliance_data()
|
|
1814
|
+
|
|
1815
|
+
# Step 1: Sync assets first
|
|
1816
|
+
self._sync_assets()
|
|
1817
|
+
|
|
1818
|
+
# Step 2: CRITICAL - Pre-populate control implementation cache BEFORE creating assessments
|
|
1819
|
+
logger.info("🔧 Pre-populating control implementation cache for issue processing...")
|
|
1820
|
+
self._populate_control_implementation_cache()
|
|
1821
|
+
|
|
1822
|
+
# Step 3: Create control assessments BEFORE issues (ensures assessmentId is available)
|
|
1823
|
+
logger.info("🔧 Creating control assessments BEFORE issue processing...")
|
|
1824
|
+
self._sync_control_assessments()
|
|
1825
|
+
|
|
1826
|
+
# Step 3.5: CRITICAL - Refresh assessment cache after assessments are created
|
|
1827
|
+
logger.info("🔧 Refreshing assessment cache with newly created assessments...")
|
|
1828
|
+
self._refresh_assessment_cache_after_creation()
|
|
1829
|
+
|
|
1830
|
+
# Step 4: NOW process issues with controlId and assessmentId properly set
|
|
1831
|
+
logger.info("🔧 Processing issues with control and assessment IDs available...")
|
|
1832
|
+
self._sync_issues()
|
|
1833
|
+
|
|
1834
|
+
self._finalize_scan_history(scan_history)
|
|
1835
|
+
|
|
1836
|
+
logger.info(f"Completed {self.title} compliance sync with proper assessment ordering")
|
|
1837
|
+
|
|
1838
|
+
except Exception as e:
|
|
1839
|
+
error_and_exit(f"Error during compliance sync: {e}")
|
|
1840
|
+
|
|
1518
1841
|
def sync_policy_compliance(self, create_issues: bool = None, update_control_status: bool = None) -> None:
|
|
1519
1842
|
"""
|
|
1520
1843
|
Main method to sync policy compliance data from Wiz.
|
|
@@ -1543,8 +1866,11 @@ class WizPolicyComplianceIntegration(ComplianceIntegration):
|
|
|
1543
1866
|
if update_control_status is not None:
|
|
1544
1867
|
self.update_control_status = update_control_status
|
|
1545
1868
|
|
|
1546
|
-
# Step 3:
|
|
1547
|
-
|
|
1869
|
+
# Step 3: Sync using the overridden method (which ensures proper ordering)
|
|
1870
|
+
logger.info(
|
|
1871
|
+
f"🔧 Sync parameters: create_issues={self.create_issues}, update_control_status={self.update_control_status}"
|
|
1872
|
+
)
|
|
1873
|
+
|
|
1548
1874
|
self.sync_compliance()
|
|
1549
1875
|
|
|
1550
1876
|
# Step 4: Write data to JSON file for reference (post-processing)
|
|
@@ -1554,7 +1880,6 @@ class WizPolicyComplianceIntegration(ComplianceIntegration):
|
|
|
1554
1880
|
logger.info("Policy compliance sync completed successfully")
|
|
1555
1881
|
|
|
1556
1882
|
except Exception as e:
|
|
1557
|
-
logger.error(f"Policy compliance sync failed: {str(e)}")
|
|
1558
1883
|
error_and_exit(f"Policy compliance sync failed: {str(e)}")
|
|
1559
1884
|
|
|
1560
1885
|
def sync_wiz_compliance(self) -> None:
|
|
@@ -1587,180 +1912,1054 @@ class WizPolicyComplianceIntegration(ComplianceIntegration):
|
|
|
1587
1912
|
finding: IntegrationFinding,
|
|
1588
1913
|
) -> regscale_models.Issue:
|
|
1589
1914
|
"""
|
|
1590
|
-
Create/update the issue
|
|
1915
|
+
Create/update the issue with ALL fields set BEFORE saving.
|
|
1916
|
+
|
|
1917
|
+
This method ensures proper data flow:
|
|
1918
|
+
1. Check for existing issues to prevent duplicates
|
|
1919
|
+
2. Pre-populate compliance fields on the finding
|
|
1920
|
+
3. Use parent class logic which saves with all fields set
|
|
1591
1921
|
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
- Ensure remediationDescription contains Wiz remediationInstructions
|
|
1922
|
+
This fixes the duplicate issue creation problem by using proper
|
|
1923
|
+
duplicate detection and avoids double-saving.
|
|
1595
1924
|
"""
|
|
1596
|
-
#
|
|
1597
|
-
|
|
1598
|
-
issue = super().create_or_update_issue_from_finding(title, finding)
|
|
1925
|
+
# Load cache if not already loaded for duplicate detection
|
|
1926
|
+
self._load_existing_records_cache()
|
|
1599
1927
|
|
|
1600
|
-
#
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
issue.assetIdentifier = self._compute_consolidated_asset_identifier(issue, finding)
|
|
1604
|
-
self._set_control_and_assessment_ids(issue, finding)
|
|
1605
|
-
if getattr(self, "create_poams", False):
|
|
1606
|
-
issue.isPoam = True
|
|
1607
|
-
self._reparent_issue_to_asset(issue, finding)
|
|
1608
|
-
issue.save(bulk=True)
|
|
1609
|
-
except Exception as e:
|
|
1610
|
-
logger.error(f"Error in post-issue processing: {e}")
|
|
1611
|
-
import traceback
|
|
1928
|
+
# CRITICAL: Pre-populate compliance fields on the finding BEFORE parent call
|
|
1929
|
+
# This ensures the parent class saves the issue with all fields already set
|
|
1930
|
+
self._populate_compliance_fields_on_finding(finding)
|
|
1612
1931
|
|
|
1613
|
-
|
|
1932
|
+
# CRITICAL FIX: If assessment_id is set, prepare the finding for assessment parenting
|
|
1933
|
+
if hasattr(finding, "assessment_id") and finding.assessment_id:
|
|
1934
|
+
assessment_id = finding.assessment_id
|
|
1935
|
+
logger.debug(f"🔄 PRE-SETTING ASSESSMENT PARENT: assessmentId={assessment_id}")
|
|
1614
1936
|
|
|
1615
|
-
|
|
1937
|
+
# Add parent override fields to the finding for the ScannerIntegration to use
|
|
1938
|
+
finding._override_parent_id = assessment_id
|
|
1939
|
+
finding._override_parent_module = "assessments"
|
|
1616
1940
|
|
|
1617
|
-
|
|
1618
|
-
|
|
1941
|
+
logger.debug(f" ✅ Finding will use parent: assessments #{assessment_id}")
|
|
1942
|
+
|
|
1943
|
+
# Check for existing issue by external_id first
|
|
1944
|
+
external_id = finding.external_id
|
|
1945
|
+
existing_issue = self._find_existing_issue_cached(external_id)
|
|
1946
|
+
|
|
1947
|
+
if existing_issue:
|
|
1948
|
+
return self._update_existing_issue_with_compliance_fields(existing_issue, title, finding)
|
|
1949
|
+
else:
|
|
1950
|
+
# Set finding context for our override method to access
|
|
1951
|
+
self._current_finding_context = finding
|
|
1952
|
+
try:
|
|
1953
|
+
# Parent class will now create/save the issue with compliance fields already set
|
|
1954
|
+
return super().create_or_update_issue_from_finding(title, finding)
|
|
1955
|
+
finally:
|
|
1956
|
+
# Clean up context
|
|
1957
|
+
if hasattr(self, "_current_finding_context"):
|
|
1958
|
+
delattr(self, "_current_finding_context")
|
|
1959
|
+
|
|
1960
|
+
def _update_existing_issue_with_compliance_fields(
|
|
1961
|
+
self, existing_issue: regscale_models.Issue, title: str, finding: IntegrationFinding
|
|
1962
|
+
) -> regscale_models.Issue:
|
|
1619
1963
|
"""
|
|
1620
|
-
Update
|
|
1964
|
+
Update existing issue with basic fields and enhance with compliance-specific fields.
|
|
1621
1965
|
|
|
1622
|
-
:param
|
|
1623
|
-
:param
|
|
1624
|
-
:
|
|
1625
|
-
:
|
|
1966
|
+
:param existing_issue: The existing issue to update
|
|
1967
|
+
:param title: New issue title
|
|
1968
|
+
:param finding: Finding with updated data
|
|
1969
|
+
:return: Updated issue with all fields set
|
|
1970
|
+
"""
|
|
1971
|
+
|
|
1972
|
+
# Update basic fields (similar to parent class logic)
|
|
1973
|
+
existing_issue.title = title
|
|
1974
|
+
existing_issue.description = finding.description
|
|
1975
|
+
existing_issue.severity = finding.severity
|
|
1976
|
+
existing_issue.status = finding.status
|
|
1977
|
+
existing_issue.dateLastUpdated = self.scan_date
|
|
1978
|
+
|
|
1979
|
+
# Set control-related field
|
|
1980
|
+
if getattr(finding, "control_labels", None):
|
|
1981
|
+
existing_issue.affectedControls = ",".join(finding.control_labels)
|
|
1982
|
+
elif getattr(finding, "affected_controls", None):
|
|
1983
|
+
existing_issue.affectedControls = finding.affected_controls
|
|
1984
|
+
|
|
1985
|
+
# Enhance with compliance-specific fields
|
|
1986
|
+
self._enhance_issue_with_compliance_fields(existing_issue, finding)
|
|
1987
|
+
|
|
1988
|
+
# CRITICAL FIX: Handle assessment parenting for existing issues too
|
|
1989
|
+
if hasattr(finding, "assessment_id") and finding.assessment_id:
|
|
1990
|
+
assessment_id = finding.assessment_id
|
|
1991
|
+
|
|
1992
|
+
# Set assessment as the parent
|
|
1993
|
+
existing_issue.parentId = assessment_id
|
|
1994
|
+
existing_issue.parentModule = "assessments"
|
|
1995
|
+
existing_issue.assessmentId = assessment_id
|
|
1996
|
+
|
|
1997
|
+
existing_issue.save()
|
|
1998
|
+
|
|
1999
|
+
return existing_issue
|
|
2000
|
+
|
|
2001
|
+
def _create_or_update_issue(
|
|
2002
|
+
self,
|
|
2003
|
+
finding: IntegrationFinding,
|
|
2004
|
+
issue_status,
|
|
2005
|
+
title: str,
|
|
2006
|
+
existing_issue=None,
|
|
2007
|
+
):
|
|
2008
|
+
"""
|
|
2009
|
+
Override parent method to handle assessment parenting correctly.
|
|
2010
|
+
|
|
2011
|
+
CRITICAL FIX: Check if the finding has assessment parent overrides and apply them.
|
|
1626
2012
|
"""
|
|
1627
|
-
|
|
2013
|
+
# Get consolidated asset identifier
|
|
2014
|
+
asset_identifier = self.get_consolidated_asset_identifier(finding, existing_issue)
|
|
2015
|
+
|
|
2016
|
+
# Prepare issue data
|
|
2017
|
+
issue_title = self.get_issue_title(finding) or title
|
|
2018
|
+
description = finding.description or ""
|
|
2019
|
+
remediation_description = finding.recommendation_for_mitigation or finding.remediation or ""
|
|
2020
|
+
is_poam = self.is_poam(finding)
|
|
2021
|
+
|
|
2022
|
+
if existing_issue:
|
|
2023
|
+
logger.debug(
|
|
2024
|
+
"Updating existing issue %s with assetIdentifier %s", existing_issue.id, finding.asset_identifier
|
|
2025
|
+
)
|
|
2026
|
+
|
|
2027
|
+
# If we have an existing issue, update its fields instead of creating a new one
|
|
2028
|
+
issue = existing_issue or regscale_models.Issue()
|
|
2029
|
+
|
|
2030
|
+
# CRITICAL FIX: Check for parent overrides from the finding
|
|
2031
|
+
if hasattr(finding, "_override_parent_id") and hasattr(finding, "_override_parent_module"):
|
|
2032
|
+
parent_id = finding._override_parent_id
|
|
2033
|
+
parent_module = finding._override_parent_module
|
|
2034
|
+
logger.debug(f"🔄 USING OVERRIDE PARENT: {parent_module} #{parent_id}")
|
|
2035
|
+
else:
|
|
2036
|
+
parent_id = self.plan_id
|
|
2037
|
+
parent_module = self.parent_module
|
|
2038
|
+
|
|
2039
|
+
# Update all fields (copying from ScannerIntegration but with override parent)
|
|
2040
|
+
issue.parentId = parent_id
|
|
2041
|
+
issue.parentModule = parent_module
|
|
2042
|
+
issue.vulnerabilityId = finding.vulnerability_id
|
|
2043
|
+
issue.title = issue_title
|
|
2044
|
+
issue.dateCreated = finding.date_created
|
|
2045
|
+
issue.status = issue_status
|
|
2046
|
+
issue.dateCompleted = (
|
|
2047
|
+
self.get_date_completed(finding, issue_status)
|
|
2048
|
+
if issue_status == regscale_models.IssueStatus.Closed
|
|
2049
|
+
else None
|
|
2050
|
+
)
|
|
2051
|
+
issue.severityLevel = finding.severity
|
|
2052
|
+
issue.issueOwnerId = self.assessor_id
|
|
2053
|
+
issue.securityPlanId = self.plan_id if not self.is_component else None
|
|
2054
|
+
issue.identification = finding.identification
|
|
2055
|
+
issue.dateFirstDetected = finding.first_seen
|
|
2056
|
+
|
|
2057
|
+
# Ensure a due date is always set using configured policy defaults (e.g., FedRAMP)
|
|
2058
|
+
if not finding.due_date:
|
|
2059
|
+
try:
|
|
2060
|
+
base_created = finding.date_created or issue.dateCreated
|
|
2061
|
+
finding.due_date = issue_due_date(
|
|
2062
|
+
severity=finding.severity,
|
|
2063
|
+
created_date=base_created,
|
|
2064
|
+
title=self.title,
|
|
2065
|
+
)
|
|
2066
|
+
except Exception:
|
|
2067
|
+
# Final fallback to a Low severity default if anything goes wrong
|
|
2068
|
+
base_created = finding.date_created or issue.dateCreated
|
|
2069
|
+
finding.due_date = issue_due_date(
|
|
2070
|
+
severity=regscale_models.IssueSeverity.Low,
|
|
2071
|
+
created_date=base_created,
|
|
2072
|
+
title=self.title,
|
|
2073
|
+
)
|
|
2074
|
+
issue.dueDate = finding.due_date
|
|
2075
|
+
issue.description = description
|
|
2076
|
+
issue.sourceReport = finding.source_report or self.title
|
|
2077
|
+
issue.recommendedActions = finding.recommendation_for_mitigation
|
|
2078
|
+
issue.assetIdentifier = asset_identifier
|
|
2079
|
+
issue.securityChecks = finding.security_check or finding.external_id
|
|
2080
|
+
issue.remediationDescription = remediation_description
|
|
2081
|
+
issue.integrationFindingId = self.get_finding_identifier(finding)
|
|
2082
|
+
issue.poamComments = finding.poam_comments
|
|
2083
|
+
issue.cve = finding.cve
|
|
2084
|
+
|
|
2085
|
+
# CRITICAL: Set assessmentId (this is the key fix)
|
|
2086
|
+
issue.assessmentId = finding.assessment_id
|
|
2087
|
+
logger.debug(f"✅ SETTING assessmentId = {finding.assessment_id} with parent = {parent_module} #{parent_id}")
|
|
2088
|
+
|
|
2089
|
+
control_id = self.get_control_implementation_id_for_cci(finding.cci_ref) if finding.cci_ref else None
|
|
2090
|
+
issue.controlId = control_id
|
|
2091
|
+
|
|
2092
|
+
# Add the control implementation ids and the cci ref if it exists
|
|
2093
|
+
cci_control_ids = [control_id] if control_id is not None else []
|
|
2094
|
+
if finding.affected_controls:
|
|
1628
2095
|
issue.affectedControls = finding.affected_controls
|
|
1629
|
-
elif
|
|
1630
|
-
issue.affectedControls = ",".join(finding.control_labels)
|
|
2096
|
+
elif finding.control_labels:
|
|
2097
|
+
issue.affectedControls = ", ".join(sorted({cl for cl in finding.control_labels if cl}))
|
|
2098
|
+
|
|
2099
|
+
issue.controlImplementationIds = list(set(finding._control_implementation_ids + cci_control_ids)) # noqa
|
|
2100
|
+
issue.isPoam = is_poam
|
|
2101
|
+
issue.basisForAdjustment = (
|
|
2102
|
+
finding.basis_for_adjustment if finding.basis_for_adjustment else f"{self.title} import"
|
|
2103
|
+
)
|
|
2104
|
+
issue.pluginId = finding.plugin_id
|
|
2105
|
+
issue.originalRiskRating = regscale_models.Issue.assign_risk_rating(finding.severity)
|
|
2106
|
+
issue.changes = "<p>Current: {}</p><p>Planned: {}</p>".format(
|
|
2107
|
+
finding.milestone_changes, finding.planned_milestone_changes
|
|
2108
|
+
)
|
|
2109
|
+
issue.adjustedRiskRating = finding.adjusted_risk_rating
|
|
2110
|
+
issue.riskAdjustment = finding.risk_adjustment
|
|
2111
|
+
issue.operationalRequirement = finding.operational_requirements
|
|
2112
|
+
issue.deviationRationale = finding.deviation_rationale
|
|
2113
|
+
issue.dateLastUpdated = get_current_datetime()
|
|
2114
|
+
issue.affectedControls = finding.affected_controls
|
|
2115
|
+
|
|
2116
|
+
if finding.cve:
|
|
2117
|
+
issue = self.lookup_kev_and_update_issue(cve=finding.cve, issue=issue, cisa_kevs=self._kev_data)
|
|
2118
|
+
|
|
2119
|
+
if existing_issue:
|
|
2120
|
+
logger.debug(f"💾 Saving existing issue {issue.id} with assessmentId={issue.assessmentId}")
|
|
2121
|
+
issue.save(bulk=True)
|
|
2122
|
+
else:
|
|
2123
|
+
logger.info(f"💾 Creating new issue with assessmentId={issue.assessmentId}")
|
|
2124
|
+
issue = issue.create_or_update(
|
|
2125
|
+
bulk_update=True, defaults={"otherIdentifier": self._get_other_identifier(finding, is_poam)}
|
|
2126
|
+
)
|
|
2127
|
+
self.extra_data_to_properties(finding, issue.id)
|
|
2128
|
+
|
|
2129
|
+
self._handle_property_and_milestone_creation(issue, finding, existing_issue)
|
|
2130
|
+
return issue
|
|
1631
2131
|
|
|
1632
|
-
def
|
|
2132
|
+
def _populate_compliance_fields_on_finding(self, finding: IntegrationFinding) -> None:
|
|
1633
2133
|
"""
|
|
1634
|
-
|
|
2134
|
+
Pre-populate compliance-specific fields on the finding before issue creation.
|
|
1635
2135
|
|
|
1636
|
-
|
|
2136
|
+
This ensures controlId and assessmentId are set on the finding object
|
|
2137
|
+
so the parent class can save the issue with all fields in one operation.
|
|
1637
2138
|
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
2139
|
+
The parent class expects:
|
|
2140
|
+
- finding.assessment_id -> issue.assessmentId
|
|
2141
|
+
- finding.cci_ref -> calls get_control_implementation_id_for_cci() -> issue.controlId
|
|
2142
|
+
|
|
2143
|
+
:param finding: Finding to populate with compliance fields
|
|
1642
2144
|
"""
|
|
1643
|
-
delimiter = "\n"
|
|
1644
|
-
identifiers: set[str] = set()
|
|
1645
|
-
# Collect identifiers from all failed items matching this control
|
|
1646
2145
|
try:
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
2146
|
+
# Set compliance fields on the finding itself before issue creation
|
|
2147
|
+
if hasattr(finding, "rule_id") and finding.rule_id:
|
|
2148
|
+
control_id = self._normalize_control_id_string(finding.rule_id)
|
|
2149
|
+
if control_id:
|
|
2150
|
+
|
|
2151
|
+
# Get control implementation ID
|
|
2152
|
+
impl_id = self._issue_field_setter._get_or_find_implementation_id(control_id)
|
|
2153
|
+
if impl_id:
|
|
2154
|
+
# Store the control ID as cci_ref so parent class calls our override method
|
|
2155
|
+
finding.cci_ref = control_id
|
|
2156
|
+
# Cache the implementation ID for our override method
|
|
2157
|
+
finding._wiz_control_implementation_id = impl_id
|
|
2158
|
+
|
|
2159
|
+
# Get assessment ID and set it on the finding (parent class uses this directly)
|
|
2160
|
+
assess_id = self._issue_field_setter._get_or_find_assessment_id(impl_id)
|
|
2161
|
+
if assess_id:
|
|
2162
|
+
finding.assessment_id = assess_id
|
|
1656
2163
|
except Exception:
|
|
1657
2164
|
pass
|
|
1658
|
-
# Merge with existing identifiers and current finding
|
|
1659
|
-
if issue.assetIdentifier:
|
|
1660
|
-
identifiers |= {e for e in (issue.assetIdentifier or "").split(delimiter) if e}
|
|
1661
|
-
if finding.asset_identifier:
|
|
1662
|
-
identifiers.add(finding.asset_identifier)
|
|
1663
|
-
return delimiter.join(sorted(identifiers))
|
|
1664
2165
|
|
|
1665
|
-
def
|
|
2166
|
+
def _enhance_issue_with_compliance_fields(self, issue: regscale_models.Issue, finding: IntegrationFinding) -> None:
|
|
1666
2167
|
"""
|
|
1667
|
-
|
|
2168
|
+
Enhance an issue with compliance-specific fields (controlId and assessmentId).
|
|
1668
2169
|
|
|
1669
|
-
:
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
:
|
|
2170
|
+
NOTE: This method is now primarily for the existing issue update path.
|
|
2171
|
+
New issues should have fields set via _populate_compliance_fields_on_finding.
|
|
2172
|
+
|
|
2173
|
+
:param issue: Issue object to enhance
|
|
2174
|
+
:param finding: Finding with control data
|
|
1673
2175
|
"""
|
|
1674
2176
|
try:
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
if not assess_id and impl_id and hasattr(self, "_assessment_by_impl_today"):
|
|
1683
|
-
assess = self._assessment_by_impl_today.get(impl_id)
|
|
1684
|
-
assess_id = assess.id if assess else None
|
|
1685
|
-
if assess_id:
|
|
1686
|
-
issue.assessmentId = assess_id
|
|
2177
|
+
# Set control implementation and assessment IDs using our field setter
|
|
2178
|
+
if hasattr(finding, "rule_id") and finding.rule_id:
|
|
2179
|
+
control_id = self._normalize_control_id_string(finding.rule_id)
|
|
2180
|
+
if control_id:
|
|
2181
|
+
result = self._issue_field_setter.set_control_and_assessment_ids(issue, control_id)
|
|
2182
|
+
if not result.success:
|
|
2183
|
+
logger.warning(f"Failed to set compliance fields for '{control_id}': {result.error_message}")
|
|
1687
2184
|
except Exception:
|
|
1688
2185
|
pass
|
|
1689
2186
|
|
|
1690
|
-
def
|
|
2187
|
+
def get_control_implementation_id_for_cci(self, cci: Optional[str]) -> Optional[int]:
|
|
2188
|
+
"""
|
|
2189
|
+
Override parent method to return control implementation ID for Wiz control IDs.
|
|
2190
|
+
|
|
2191
|
+
The parent class calls this method when finding.cci_ref is set, and uses the
|
|
2192
|
+
returned value to set issue.controlId. We store our control implementation
|
|
2193
|
+
ID on the finding and return it here.
|
|
2194
|
+
|
|
2195
|
+
:param cci: Control identifier (e.g., 'AC-2(1)') stored in finding.cci_ref
|
|
2196
|
+
:return: Control implementation ID if found, None otherwise
|
|
1691
2197
|
"""
|
|
1692
|
-
|
|
2198
|
+
# Check if this is a call with our cached implementation ID on the current finding
|
|
2199
|
+
if hasattr(self, "_current_finding_context"):
|
|
2200
|
+
finding = self._current_finding_context
|
|
2201
|
+
if (
|
|
2202
|
+
hasattr(finding, "_wiz_control_implementation_id")
|
|
2203
|
+
and hasattr(finding, "cci_ref")
|
|
2204
|
+
and finding.cci_ref == cci
|
|
2205
|
+
):
|
|
2206
|
+
impl_id = finding._wiz_control_implementation_id
|
|
2207
|
+
return impl_id
|
|
2208
|
+
|
|
2209
|
+
# Fallback: try to look it up directly (for edge cases)
|
|
2210
|
+
if cci:
|
|
2211
|
+
control_id = self._normalize_control_id_string(cci)
|
|
2212
|
+
if control_id:
|
|
2213
|
+
impl_id = self._issue_field_setter._get_or_find_implementation_id(control_id)
|
|
2214
|
+
if impl_id:
|
|
2215
|
+
return impl_id
|
|
2216
|
+
|
|
2217
|
+
# Final fallback to parent class behavior
|
|
2218
|
+
return super().get_control_implementation_id_for_cci(cci)
|
|
2219
|
+
|
|
2220
|
+
def _populate_control_implementation_cache(self) -> None:
|
|
2221
|
+
"""
|
|
2222
|
+
Pre-populate the control implementation and assessment caches.
|
|
2223
|
+
|
|
2224
|
+
CRITICAL: This ensures controlId and assessmentId can be reliably set on issues.
|
|
2225
|
+
This method loads control implementations and their associated assessments into
|
|
2226
|
+
cache to enable fast lookups during issue processing.
|
|
1693
2227
|
|
|
1694
|
-
:param regscale_models.Issue issue: Issue to reparent
|
|
1695
|
-
:param IntegrationFinding finding: Finding with asset identifier
|
|
1696
2228
|
:return: None
|
|
1697
2229
|
:rtype: None
|
|
1698
2230
|
"""
|
|
1699
2231
|
try:
|
|
1700
|
-
|
|
1701
|
-
if not asset:
|
|
1702
|
-
asset = self._ensure_asset_for_finding(finding)
|
|
1703
|
-
if asset and getattr(asset, "id", None):
|
|
1704
|
-
issue.parentId = asset.id
|
|
1705
|
-
issue.parentModule = "assets"
|
|
1706
|
-
except Exception:
|
|
1707
|
-
# If asset lookup fails, keep existing parent
|
|
1708
|
-
pass
|
|
2232
|
+
from regscale.models import regscale_models
|
|
1709
2233
|
|
|
1710
|
-
|
|
2234
|
+
logger.info("🔍 Pre-populating control implementation cache for issue processing...")
|
|
2235
|
+
|
|
2236
|
+
# Get all control implementations for this plan
|
|
2237
|
+
implementations = regscale_models.ControlImplementation.get_all_by_parent(
|
|
2238
|
+
parent_id=self.plan_id, parent_module=self.parent_module
|
|
2239
|
+
)
|
|
2240
|
+
|
|
2241
|
+
if not implementations:
|
|
2242
|
+
logger.warning("No control implementations found for this plan")
|
|
2243
|
+
return
|
|
2244
|
+
|
|
2245
|
+
logger.info(f"Found {len(implementations)} control implementations to cache")
|
|
2246
|
+
|
|
2247
|
+
# Cache SecurityControl lookups to avoid repeated API calls
|
|
2248
|
+
security_control_cache = {}
|
|
2249
|
+
controls_mapped = 0
|
|
2250
|
+
assessments_mapped = 0
|
|
2251
|
+
|
|
2252
|
+
for impl in implementations:
|
|
2253
|
+
try:
|
|
2254
|
+
# Skip if no controlID reference
|
|
2255
|
+
if not hasattr(impl, "controlID") or not impl.controlID:
|
|
2256
|
+
continue
|
|
2257
|
+
|
|
2258
|
+
# Get or cache the security control
|
|
2259
|
+
if impl.controlID not in security_control_cache:
|
|
2260
|
+
security_control = regscale_models.SecurityControl.get_object(object_id=impl.controlID)
|
|
2261
|
+
security_control_cache[impl.controlID] = security_control
|
|
2262
|
+
else:
|
|
2263
|
+
security_control = security_control_cache[impl.controlID]
|
|
2264
|
+
|
|
2265
|
+
if security_control and hasattr(security_control, "controlId"):
|
|
2266
|
+
# Normalize and cache the control ID mapping
|
|
2267
|
+
normalized_id = self._normalize_control_id_string(security_control.controlId)
|
|
2268
|
+
if normalized_id:
|
|
2269
|
+
self._impl_id_by_control[normalized_id] = impl.id
|
|
2270
|
+
controls_mapped += 1
|
|
2271
|
+
|
|
2272
|
+
# Also try to cache the most recent assessment
|
|
2273
|
+
try:
|
|
2274
|
+
assessments = regscale_models.Assessment.get_all_by_parent(
|
|
2275
|
+
parent_id=impl.id, parent_module="controls"
|
|
2276
|
+
)
|
|
2277
|
+
if assessments:
|
|
2278
|
+
# Get the most recent assessment
|
|
2279
|
+
assessments.sort(key=lambda a: a.id if hasattr(a, "id") else 0, reverse=True)
|
|
2280
|
+
self._assessment_by_impl_today[impl.id] = assessments[0]
|
|
2281
|
+
assessments_mapped += 1
|
|
2282
|
+
except Exception:
|
|
2283
|
+
pass
|
|
2284
|
+
|
|
2285
|
+
except Exception:
|
|
2286
|
+
continue
|
|
2287
|
+
|
|
2288
|
+
logger.info("✓ Control implementation cache populated:")
|
|
2289
|
+
logger.info(f" - {controls_mapped} control ID mappings")
|
|
2290
|
+
logger.info(f" - {assessments_mapped} assessment mappings")
|
|
2291
|
+
|
|
2292
|
+
except Exception as e:
|
|
2293
|
+
logger.error(f"Error populating control implementation cache: {e}")
|
|
2294
|
+
|
|
2295
|
+
def _refresh_assessment_cache_after_creation(self) -> None:
|
|
1711
2296
|
"""
|
|
1712
|
-
|
|
2297
|
+
Refresh the assessment cache after control assessments have been created.
|
|
2298
|
+
|
|
2299
|
+
CRITICAL: This ensures that newly created assessments from the sync_control_assessments
|
|
2300
|
+
step are available when processing issues. Without this, assessmentId will not be set
|
|
2301
|
+
on issues because the cache only contains old assessments.
|
|
1713
2302
|
|
|
1714
|
-
:
|
|
2303
|
+
:return: None
|
|
2304
|
+
:rtype: None
|
|
1715
2305
|
"""
|
|
1716
2306
|
try:
|
|
1717
|
-
from regscale.
|
|
2307
|
+
from regscale.models import regscale_models
|
|
2308
|
+
from datetime import datetime
|
|
2309
|
+
|
|
2310
|
+
logger.info("🔄 Refreshing assessment cache with newly created assessments...")
|
|
1718
2311
|
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
severity_counts = {"Critical": 0, "High": 0, "Moderate": 0, "Low": 0}
|
|
2312
|
+
refreshed_count = 0
|
|
2313
|
+
today = datetime.now().date()
|
|
1722
2314
|
|
|
1723
|
-
for
|
|
2315
|
+
# Only refresh assessments for implementations we know about
|
|
2316
|
+
for control_id, impl_id in self._impl_id_by_control.items():
|
|
1724
2317
|
try:
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
2318
|
+
# Get all assessments for this implementation
|
|
2319
|
+
assessments = regscale_models.Assessment.get_all_by_parent(
|
|
2320
|
+
parent_id=impl_id, parent_module="controls"
|
|
2321
|
+
)
|
|
2322
|
+
|
|
2323
|
+
if not assessments:
|
|
1728
2324
|
continue
|
|
1729
|
-
|
|
1730
|
-
|
|
2325
|
+
|
|
2326
|
+
# Find today's assessment (most recent created today)
|
|
2327
|
+
today_assessments = []
|
|
2328
|
+
for assessment in assessments:
|
|
2329
|
+
assessment_date = None
|
|
2330
|
+
try:
|
|
2331
|
+
# Try to get assessment date from various fields
|
|
2332
|
+
date_fields = ["actualFinish", "plannedFinish", "dateCreated"]
|
|
2333
|
+
for field in date_fields:
|
|
2334
|
+
if hasattr(assessment, field) and getattr(assessment, field):
|
|
2335
|
+
date_value = getattr(assessment, field)
|
|
2336
|
+
if isinstance(date_value, str):
|
|
2337
|
+
from regscale.core.app.utils.app_utils import regscale_string_to_datetime
|
|
2338
|
+
|
|
2339
|
+
assessment_date = regscale_string_to_datetime(date_value).date()
|
|
2340
|
+
elif hasattr(date_value, "date"):
|
|
2341
|
+
assessment_date = date_value.date()
|
|
2342
|
+
else:
|
|
2343
|
+
assessment_date = date_value
|
|
2344
|
+
break
|
|
2345
|
+
|
|
2346
|
+
if assessment_date == today:
|
|
2347
|
+
today_assessments.append(assessment)
|
|
2348
|
+
except Exception:
|
|
2349
|
+
continue
|
|
2350
|
+
|
|
2351
|
+
# Use most recent today's assessment, or fallback to most recent overall
|
|
2352
|
+
if today_assessments:
|
|
2353
|
+
best_assessment = max(today_assessments, key=lambda a: getattr(a, "id", 0))
|
|
2354
|
+
else:
|
|
2355
|
+
best_assessment = max(assessments, key=lambda a: getattr(a, "id", 0))
|
|
2356
|
+
|
|
2357
|
+
# Update the cache
|
|
2358
|
+
self._assessment_by_impl_today[impl_id] = best_assessment
|
|
2359
|
+
refreshed_count += 1
|
|
2360
|
+
|
|
2361
|
+
except Exception:
|
|
2362
|
+
continue
|
|
2363
|
+
|
|
2364
|
+
logger.info(f"✓ Assessment cache refreshed: {refreshed_count} assessments updated")
|
|
2365
|
+
|
|
2366
|
+
except Exception as e:
|
|
2367
|
+
logger.error(f"Error refreshing assessment cache: {e}")
|
|
2368
|
+
|
|
2369
|
+
def _find_control_implementation_id(self, control_id: str) -> Optional[int]:
|
|
2370
|
+
"""
|
|
2371
|
+
Find control implementation ID by querying the database directly.
|
|
2372
|
+
OPTIMIZED: Uses controlID field directly and caches SecurityControl lookups.
|
|
2373
|
+
|
|
2374
|
+
:param str control_id: Normalized control ID (e.g., 'AC-2(1)')
|
|
2375
|
+
:return: Control implementation ID if found
|
|
2376
|
+
:rtype: Optional[int]
|
|
2377
|
+
"""
|
|
2378
|
+
try:
|
|
2379
|
+
from regscale.models import regscale_models
|
|
2380
|
+
|
|
2381
|
+
# First check cache
|
|
2382
|
+
if hasattr(self, "_impl_id_by_control") and control_id in self._impl_id_by_control:
|
|
2383
|
+
cached_id = self._impl_id_by_control[control_id]
|
|
2384
|
+
return cached_id
|
|
2385
|
+
|
|
2386
|
+
# Get all control implementations for this plan
|
|
2387
|
+
implementations = regscale_models.ControlImplementation.get_all_by_parent(
|
|
2388
|
+
parent_id=self.plan_id, parent_module=self.parent_module
|
|
2389
|
+
)
|
|
2390
|
+
|
|
2391
|
+
# Create a cache for SecurityControl lookups to avoid repeated API calls
|
|
2392
|
+
security_control_cache = {}
|
|
2393
|
+
|
|
2394
|
+
for impl in implementations:
|
|
2395
|
+
try:
|
|
2396
|
+
# Use controlID field which references the SecurityControl
|
|
2397
|
+
if not hasattr(impl, "controlID") or not impl.controlID:
|
|
1731
2398
|
continue
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
elif sev == regscale_models.IssueSeverity.High:
|
|
1738
|
-
severity_counts["High"] += 1
|
|
1739
|
-
elif sev == regscale_models.IssueSeverity.Moderate:
|
|
1740
|
-
severity_counts["Moderate"] += 1
|
|
2399
|
+
|
|
2400
|
+
# Check if we've already looked up this security control
|
|
2401
|
+
if impl.controlID not in security_control_cache:
|
|
2402
|
+
security_control = regscale_models.SecurityControl.get_object(object_id=impl.controlID)
|
|
2403
|
+
security_control_cache[impl.controlID] = security_control
|
|
1741
2404
|
else:
|
|
1742
|
-
|
|
2405
|
+
security_control = security_control_cache[impl.controlID]
|
|
2406
|
+
|
|
2407
|
+
if security_control and hasattr(security_control, "controlId"):
|
|
2408
|
+
impl_control_id = self._normalize_control_id_string(security_control.controlId)
|
|
2409
|
+
|
|
2410
|
+
if impl_control_id == control_id:
|
|
2411
|
+
logger.info(f"✓ Found control implementation {impl.id} for control {control_id}")
|
|
2412
|
+
# Cache it for future lookups
|
|
2413
|
+
if not hasattr(self, "_impl_id_by_control"):
|
|
2414
|
+
self._impl_id_by_control = {}
|
|
2415
|
+
self._impl_id_by_control[control_id] = impl.id
|
|
2416
|
+
return impl.id
|
|
1743
2417
|
except Exception:
|
|
1744
2418
|
continue
|
|
1745
2419
|
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
2420
|
+
logger.warning(
|
|
2421
|
+
f"⚠️ No control implementation found for control {control_id} among {len(implementations)} implementations"
|
|
2422
|
+
)
|
|
2423
|
+
return None
|
|
2424
|
+
except Exception as e:
|
|
2425
|
+
logger.error(f"Error finding control implementation for {control_id}: {e}")
|
|
2426
|
+
return None
|
|
2427
|
+
|
|
2428
|
+
def _find_assessment_id_for_implementation(self, implementation_id: int) -> Optional[int]:
|
|
2429
|
+
"""
|
|
2430
|
+
Find the most recent assessment ID for a control implementation.
|
|
2431
|
+
IMPROVED: Better date handling and caching.
|
|
2432
|
+
|
|
2433
|
+
:param int implementation_id: Control implementation ID
|
|
2434
|
+
:return: Assessment ID if found
|
|
2435
|
+
:rtype: Optional[int]
|
|
2436
|
+
"""
|
|
2437
|
+
try:
|
|
2438
|
+
from regscale.models import regscale_models
|
|
2439
|
+
from datetime import datetime
|
|
2440
|
+
from regscale.core.app.utils.app_utils import regscale_string_to_datetime
|
|
2441
|
+
|
|
2442
|
+
# Check cache first
|
|
2443
|
+
if hasattr(self, "_assessment_by_impl_today") and implementation_id in self._assessment_by_impl_today:
|
|
2444
|
+
cached_assessment = self._assessment_by_impl_today[implementation_id]
|
|
2445
|
+
if cached_assessment and hasattr(cached_assessment, "id"):
|
|
2446
|
+
logger.debug(
|
|
2447
|
+
f"Found cached assessment {cached_assessment.id} for implementation {implementation_id}"
|
|
2448
|
+
)
|
|
2449
|
+
return cached_assessment.id
|
|
2450
|
+
|
|
2451
|
+
# Get assessments for this control implementation
|
|
2452
|
+
assessments = regscale_models.Assessment.get_all_by_parent(
|
|
2453
|
+
parent_id=implementation_id, parent_module="controls"
|
|
2454
|
+
)
|
|
2455
|
+
|
|
2456
|
+
if not assessments:
|
|
2457
|
+
logger.warning(f"No assessments found for control implementation {implementation_id}")
|
|
2458
|
+
return None
|
|
2459
|
+
|
|
2460
|
+
# Find the most recent assessment (preferably from today)
|
|
2461
|
+
today = datetime.now().date()
|
|
2462
|
+
today_assessments = []
|
|
2463
|
+
recent_assessments = []
|
|
2464
|
+
|
|
2465
|
+
for assessment in assessments:
|
|
2466
|
+
try:
|
|
2467
|
+
assessment_date = None
|
|
2468
|
+
|
|
2469
|
+
# Try multiple date fields in order of preference
|
|
2470
|
+
date_fields = ["plannedStart", "actualFinish", "plannedFinish", "dateCreated"]
|
|
2471
|
+
for field in date_fields:
|
|
2472
|
+
if hasattr(assessment, field) and getattr(assessment, field):
|
|
2473
|
+
date_value = getattr(assessment, field)
|
|
2474
|
+
if isinstance(date_value, str):
|
|
2475
|
+
assessment_date = regscale_string_to_datetime(date_value).date()
|
|
2476
|
+
elif hasattr(date_value, "date"):
|
|
2477
|
+
assessment_date = date_value.date()
|
|
2478
|
+
else:
|
|
2479
|
+
assessment_date = date_value
|
|
2480
|
+
break
|
|
2481
|
+
|
|
2482
|
+
if assessment_date:
|
|
2483
|
+
if assessment_date == today:
|
|
2484
|
+
today_assessments.append(assessment)
|
|
2485
|
+
else:
|
|
2486
|
+
recent_assessments.append((assessment, assessment_date))
|
|
2487
|
+
else:
|
|
2488
|
+
# Assessment with no parseable date
|
|
2489
|
+
recent_assessments.append((assessment, None))
|
|
2490
|
+
except Exception:
|
|
2491
|
+
recent_assessments.append((assessment, None))
|
|
2492
|
+
|
|
2493
|
+
# Prefer today's assessments
|
|
2494
|
+
if today_assessments:
|
|
2495
|
+
# Sort by ID (highest/newest first) if multiple today
|
|
2496
|
+
today_assessments.sort(key=lambda a: a.id if hasattr(a, "id") else 0, reverse=True)
|
|
2497
|
+
assessment = today_assessments[0]
|
|
2498
|
+
logger.info(
|
|
2499
|
+
f"✓ Found today's assessment {assessment.id} for control implementation {implementation_id}"
|
|
2500
|
+
)
|
|
2501
|
+
# Cache it for future lookups
|
|
2502
|
+
if not hasattr(self, "_assessment_by_impl_today"):
|
|
2503
|
+
self._assessment_by_impl_today = {}
|
|
2504
|
+
self._assessment_by_impl_today[implementation_id] = assessment
|
|
2505
|
+
return assessment.id
|
|
2506
|
+
|
|
2507
|
+
# Fall back to most recent assessment
|
|
2508
|
+
if recent_assessments:
|
|
2509
|
+
# Sort by date (newest first), handling None dates
|
|
2510
|
+
recent_assessments.sort(
|
|
2511
|
+
key=lambda x: (x[1] if x[1] else datetime.min.date(), x[0].id if hasattr(x[0], "id") else 0),
|
|
2512
|
+
reverse=True,
|
|
2513
|
+
)
|
|
2514
|
+
assessment = recent_assessments[0][0]
|
|
2515
|
+
logger.info(f"✓ Found recent assessment {assessment.id} for control implementation {implementation_id}")
|
|
2516
|
+
# Cache it even if not today's
|
|
2517
|
+
if not hasattr(self, "_assessment_by_impl_today"):
|
|
2518
|
+
self._assessment_by_impl_today = {}
|
|
2519
|
+
self._assessment_by_impl_today[implementation_id] = assessment
|
|
2520
|
+
return assessment.id
|
|
2521
|
+
|
|
2522
|
+
logger.warning(f"⚠️ No usable assessments found for control implementation {implementation_id}")
|
|
2523
|
+
return None
|
|
2524
|
+
except Exception as e:
|
|
2525
|
+
logger.error(f"Error finding assessment for control implementation {implementation_id}: {e}")
|
|
2526
|
+
return None
|
|
2527
|
+
|
|
2528
|
+
def _reparent_issue_to_asset(self, issue: regscale_models.Issue) -> None:
|
|
2529
|
+
"""
|
|
2530
|
+
Reparent issue to the control implementation instead of the security plan.
|
|
2531
|
+
This ensures issues are properly associated with their control implementations.
|
|
2532
|
+
|
|
2533
|
+
:param regscale_models.Issue issue: Issue to reparent to control implementation
|
|
2534
|
+
:param IntegrationFinding finding: Finding with control information
|
|
2535
|
+
:return: None
|
|
2536
|
+
:rtype: None
|
|
2537
|
+
"""
|
|
2538
|
+
# If we have a control implementation ID, parent the issue to it
|
|
2539
|
+
if issue.controlId:
|
|
2540
|
+
issue.parentId = issue.controlId
|
|
2541
|
+
issue.parentModule = "controls"
|
|
2542
|
+
else:
|
|
2543
|
+
# Fall back to security plan if no control implementation found
|
|
2544
|
+
pass
|
|
2545
|
+
|
|
2546
|
+
def _update_scan_history(self, scan_history: regscale_models.ScanHistory) -> None:
|
|
2547
|
+
"""
|
|
2548
|
+
No scan history updates for compliance report ingest.
|
|
2549
|
+
|
|
2550
|
+
:param regscale_models.ScanHistory scan_history: Scan history record (unused)
|
|
2551
|
+
"""
|
|
2552
|
+
# No scan history for compliance report ingest
|
|
2553
|
+
pass
|
|
2554
|
+
|
|
2555
|
+
def _process_control_assessments(self) -> None:
|
|
2556
|
+
"""
|
|
2557
|
+
Process control assessments only for controls that have validated compliance items
|
|
2558
|
+
with existing assets in RegScale. This ensures we don't create assessments for
|
|
2559
|
+
controls that have no assets in our boundary.
|
|
2560
|
+
"""
|
|
2561
|
+
logger.info("🎯 Starting control assessment processing for Wiz compliance integration")
|
|
2562
|
+
|
|
2563
|
+
# Ensure existing records cache is loaded
|
|
2564
|
+
self._load_existing_records_cache()
|
|
2565
|
+
|
|
2566
|
+
implementations = self._get_control_implementations()
|
|
2567
|
+
if not implementations:
|
|
2568
|
+
logger.warning("No control implementations found for assessment processing")
|
|
2569
|
+
return
|
|
2570
|
+
|
|
2571
|
+
# Get all potential control IDs from compliance data
|
|
2572
|
+
all_potential_controls = set(self.passing_controls.keys()) | set(self.failing_controls.keys())
|
|
2573
|
+
logger.debug(
|
|
2574
|
+
f"Found {len(all_potential_controls)} potential controls from compliance data: {sorted(all_potential_controls)}"
|
|
2575
|
+
)
|
|
2576
|
+
|
|
2577
|
+
# Validate each control has actual assets in our boundary before processing
|
|
2578
|
+
validated_controls_with_assets = {}
|
|
2579
|
+
validated_passing_controls = {}
|
|
2580
|
+
validated_failing_controls = {}
|
|
1751
2581
|
|
|
1752
|
-
|
|
1753
|
-
|
|
2582
|
+
for control_id in all_potential_controls:
|
|
2583
|
+
# Get all compliance items for this control
|
|
2584
|
+
control_items = self._get_validated_control_compliance_items(control_id)
|
|
2585
|
+
|
|
2586
|
+
if not control_items:
|
|
2587
|
+
continue
|
|
2588
|
+
|
|
2589
|
+
# Check if we have any assets for the compliance items
|
|
2590
|
+
asset_identifiers = set()
|
|
2591
|
+
assets_found = 0
|
|
2592
|
+
|
|
2593
|
+
for item in control_items:
|
|
2594
|
+
if hasattr(item, "resource_name") and item.resource_name:
|
|
2595
|
+
resource_id = getattr(item, "resource_id", "")
|
|
2596
|
+
# Verify the asset actually exists in RegScale
|
|
2597
|
+
if self._asset_exists_in_regscale(resource_id):
|
|
2598
|
+
asset_identifiers.add(item.resource_name)
|
|
2599
|
+
assets_found += 1
|
|
2600
|
+
else:
|
|
2601
|
+
logger.debug(
|
|
2602
|
+
f"Control {control_id}: Asset {resource_id} ({item.resource_name}) not found in RegScale"
|
|
2603
|
+
)
|
|
2604
|
+
logger.debug(f"Found {assets_found} valid assets for control {control_id}")
|
|
2605
|
+
if not asset_identifiers:
|
|
2606
|
+
continue
|
|
2607
|
+
|
|
2608
|
+
# This control has valid assets, include it in processing
|
|
2609
|
+
validated_controls_with_assets[control_id] = list(asset_identifiers)
|
|
2610
|
+
|
|
2611
|
+
# Preserve the pass/fail status for validated controls
|
|
2612
|
+
if control_id in self.failing_controls:
|
|
2613
|
+
validated_failing_controls[control_id] = self.failing_controls[control_id]
|
|
2614
|
+
elif control_id in self.passing_controls:
|
|
2615
|
+
validated_passing_controls[control_id] = self.passing_controls[control_id]
|
|
2616
|
+
|
|
2617
|
+
if not validated_controls_with_assets:
|
|
2618
|
+
logger.warning("❌ No controls have assets in RegScale boundary - no control assessments will be created")
|
|
2619
|
+
logger.info("📊 SUMMARY: 0 control assessments created (no assets exist in RegScale)")
|
|
2620
|
+
return
|
|
2621
|
+
|
|
2622
|
+
assessments_created = 0
|
|
2623
|
+
processed_impl_today: set[int] = set()
|
|
2624
|
+
|
|
2625
|
+
# Only process validated controls that have assets in our boundary
|
|
2626
|
+
for control_id in validated_controls_with_assets.keys():
|
|
2627
|
+
created = self._process_single_control_assessment(
|
|
2628
|
+
control_id=control_id,
|
|
2629
|
+
implementations=implementations,
|
|
2630
|
+
processed_impl_today=processed_impl_today,
|
|
2631
|
+
)
|
|
2632
|
+
assessments_created += created
|
|
2633
|
+
|
|
2634
|
+
# Calculate stats only for validated controls
|
|
2635
|
+
validated_control_ids = set(validated_controls_with_assets.keys())
|
|
2636
|
+
passing_assessments = len([cid for cid in validated_control_ids if cid not in validated_failing_controls])
|
|
2637
|
+
failing_assessments = len([cid for cid in validated_control_ids if cid in validated_failing_controls])
|
|
2638
|
+
|
|
2639
|
+
if assessments_created > 0:
|
|
1754
2640
|
logger.info(
|
|
1755
|
-
"
|
|
1756
|
-
getattr(scan_history, "id", 0),
|
|
1757
|
-
severity_counts["Critical"],
|
|
1758
|
-
severity_counts["High"],
|
|
1759
|
-
severity_counts["Moderate"],
|
|
1760
|
-
severity_counts["Low"],
|
|
2641
|
+
f"✅ Created {assessments_created} control assessments: {passing_assessments} passing, {failing_assessments} failing"
|
|
1761
2642
|
)
|
|
2643
|
+
else:
|
|
2644
|
+
logger.warning(
|
|
2645
|
+
f"⚠️ No control assessments were actually created (0 assessments) despite finding {len(validated_controls_with_assets)} controls with assets"
|
|
2646
|
+
)
|
|
2647
|
+
|
|
2648
|
+
logger.info(
|
|
2649
|
+
f"📊 CONTROL ASSESSMENT SUMMARY: {assessments_created} assessments created for {len(validated_controls_with_assets)} validated controls"
|
|
2650
|
+
)
|
|
2651
|
+
|
|
2652
|
+
def _sync_assessment_cache_from_base_class(self) -> None:
|
|
2653
|
+
"""
|
|
2654
|
+
Sync assessments from base class cache to our control cache.
|
|
2655
|
+
|
|
2656
|
+
This ensures that assessments created by the base class ComplianceIntegration
|
|
2657
|
+
are available to our IssueFieldSetter for linking issues to assessments.
|
|
2658
|
+
"""
|
|
2659
|
+
try:
|
|
2660
|
+
# Copy assessments from base class cache to our cache
|
|
2661
|
+
base_cache = getattr(self, "_assessment_by_impl_today", {})
|
|
2662
|
+
synced_count = 0
|
|
2663
|
+
|
|
2664
|
+
for impl_id, assessment in base_cache.items():
|
|
2665
|
+
self._control_cache.set_assessment(impl_id, assessment)
|
|
2666
|
+
synced_count += 1
|
|
2667
|
+
|
|
2668
|
+
logger.info(f"✅ Synced {synced_count} assessments from base class cache to control cache")
|
|
2669
|
+
|
|
2670
|
+
except Exception as e:
|
|
2671
|
+
logger.warning(f"⚠️ Failed to sync assessment cache: {e}")
|
|
2672
|
+
|
|
2673
|
+
def _get_validated_control_compliance_items(self, control_id: str) -> List[ComplianceItem]:
|
|
2674
|
+
"""
|
|
2675
|
+
Get validated compliance items for a specific control.
|
|
2676
|
+
Only returns items that have existing assets in RegScale boundary.
|
|
2677
|
+
|
|
2678
|
+
:param str control_id: Control identifier to filter by
|
|
2679
|
+
:return: List of validated compliance items for the control
|
|
2680
|
+
:rtype: List[ComplianceItem]
|
|
2681
|
+
"""
|
|
2682
|
+
validated_items: List[ComplianceItem] = []
|
|
2683
|
+
|
|
2684
|
+
for item in self.all_compliance_items:
|
|
2685
|
+
# Check if this item matches the control
|
|
2686
|
+
matches_control = False
|
|
2687
|
+
if hasattr(item, "control_ids"):
|
|
2688
|
+
item_control_ids = getattr(item, "control_ids", [])
|
|
2689
|
+
if any(cid.lower() == control_id.lower() for cid in item_control_ids):
|
|
2690
|
+
matches_control = True
|
|
2691
|
+
elif hasattr(item, "control_id") and item.control_id.lower() == control_id.lower():
|
|
2692
|
+
matches_control = True
|
|
2693
|
+
|
|
2694
|
+
if not matches_control:
|
|
2695
|
+
continue
|
|
2696
|
+
|
|
2697
|
+
# Additional validation: ensure the asset exists in RegScale
|
|
2698
|
+
resource_id = getattr(item, "resource_id", "")
|
|
2699
|
+
if resource_id and self._asset_exists_in_regscale(resource_id):
|
|
2700
|
+
validated_items.append(item)
|
|
2701
|
+
else:
|
|
2702
|
+
logger.debug(
|
|
2703
|
+
f"Filtered out compliance item for control {control_id} - asset {resource_id} not in RegScale"
|
|
2704
|
+
)
|
|
2705
|
+
|
|
2706
|
+
return validated_items
|
|
2707
|
+
|
|
2708
|
+
def _get_control_compliance_items(self, control_id: str) -> List[ComplianceItem]:
|
|
2709
|
+
"""
|
|
2710
|
+
Get all compliance items for a specific control.
|
|
2711
|
+
All items have already been filtered to framework-specific items with existing assets.
|
|
2712
|
+
|
|
2713
|
+
:param str control_id: Control identifier to filter by
|
|
2714
|
+
:return: List of compliance items for the control
|
|
2715
|
+
:rtype: List[ComplianceItem]
|
|
2716
|
+
"""
|
|
2717
|
+
items: List[ComplianceItem] = []
|
|
2718
|
+
|
|
2719
|
+
for item in self.all_compliance_items:
|
|
2720
|
+
# Check if this item matches the control
|
|
2721
|
+
matches_control = False
|
|
2722
|
+
if hasattr(item, "control_ids"):
|
|
2723
|
+
item_control_ids = getattr(item, "control_ids", [])
|
|
2724
|
+
if any(cid.lower() == control_id.lower() for cid in item_control_ids):
|
|
2725
|
+
matches_control = True
|
|
2726
|
+
elif hasattr(item, "control_id") and item.control_id.lower() == control_id.lower():
|
|
2727
|
+
matches_control = True
|
|
2728
|
+
|
|
2729
|
+
if matches_control:
|
|
2730
|
+
items.append(item)
|
|
2731
|
+
|
|
2732
|
+
return items
|
|
2733
|
+
|
|
2734
|
+
# flake8: noqa: C901
|
|
2735
|
+
def get_asset_by_identifier(self, identifier: str) -> Optional["regscale_models.Asset"]:
|
|
2736
|
+
"""
|
|
2737
|
+
Override asset lookup for Wiz policy compliance integration.
|
|
2738
|
+
|
|
2739
|
+
For policy compliance, the identifier should be the Wiz resource ID.
|
|
2740
|
+
We'll try multiple lookup strategies to find the corresponding RegScale asset.
|
|
2741
|
+
|
|
2742
|
+
:param str identifier: Asset identifier (should be Wiz resource ID)
|
|
2743
|
+
:return: Asset if found, None otherwise
|
|
2744
|
+
:rtype: Optional[regscale_models.Asset]
|
|
2745
|
+
"""
|
|
2746
|
+
|
|
2747
|
+
# First try the standard lookup by identifier (uses asset_map_by_identifier)
|
|
2748
|
+
asset = super().get_asset_by_identifier(identifier)
|
|
2749
|
+
if asset:
|
|
2750
|
+
return asset
|
|
2751
|
+
|
|
2752
|
+
# If not found, try to find using our cached RegScale assets by Wiz ID
|
|
2753
|
+
try:
|
|
2754
|
+
if hasattr(self, "_regscale_assets_by_wiz_id") and self._regscale_assets_by_wiz_id:
|
|
2755
|
+
# Direct lookup by Wiz ID (most common case)
|
|
2756
|
+
if identifier in self._regscale_assets_by_wiz_id:
|
|
2757
|
+
regscale_asset = self._regscale_assets_by_wiz_id[identifier]
|
|
2758
|
+
return regscale_asset
|
|
2759
|
+
|
|
2760
|
+
# Fallback: check all assets for name/identifier matches
|
|
2761
|
+
for wiz_id, regscale_asset in self._regscale_assets_by_wiz_id.items():
|
|
2762
|
+
# Check if asset name matches the identifier
|
|
2763
|
+
if regscale_asset.name == identifier:
|
|
2764
|
+
return regscale_asset
|
|
2765
|
+
|
|
2766
|
+
# Also check identifier field
|
|
2767
|
+
if hasattr(regscale_asset, "identifier") and regscale_asset.identifier == identifier:
|
|
2768
|
+
return regscale_asset
|
|
2769
|
+
|
|
2770
|
+
# Check other tracking number
|
|
2771
|
+
if (
|
|
2772
|
+
hasattr(regscale_asset, "otherTrackingNumber")
|
|
2773
|
+
and regscale_asset.otherTrackingNumber == identifier
|
|
2774
|
+
):
|
|
2775
|
+
logger.debug(
|
|
2776
|
+
f"Found asset via otherTrackingNumber match: {regscale_asset.name} (Wiz ID: {wiz_id})"
|
|
2777
|
+
)
|
|
2778
|
+
return regscale_asset
|
|
2779
|
+
|
|
2780
|
+
except Exception:
|
|
2781
|
+
pass
|
|
2782
|
+
|
|
2783
|
+
# Asset not found
|
|
2784
|
+
return None
|
|
2785
|
+
|
|
2786
|
+
def _ensure_asset_for_finding(self, finding: IntegrationFinding) -> Optional["regscale_models.Asset"]:
|
|
2787
|
+
"""
|
|
2788
|
+
Override asset creation for Wiz policy compliance integration.
|
|
2789
|
+
|
|
2790
|
+
We don't create assets in policy compliance integration - they come from
|
|
2791
|
+
separate Wiz inventory import. If an asset isn't found, we skip the finding.
|
|
2792
|
+
|
|
2793
|
+
:param IntegrationFinding finding: Finding that needs an asset
|
|
2794
|
+
:return: None (we don't create assets)
|
|
2795
|
+
:rtype: Optional[regscale_models.Asset]
|
|
2796
|
+
"""
|
|
2797
|
+
return None
|
|
2798
|
+
|
|
2799
|
+
def _process_consolidated_issues(self, findings: List[IntegrationFinding]) -> None:
|
|
2800
|
+
"""
|
|
2801
|
+
Process pre-consolidated findings to create issues.
|
|
2802
|
+
|
|
2803
|
+
Since fetch_findings() now creates consolidated findings (one per control with all resources),
|
|
2804
|
+
this method simply creates issues directly from each finding.
|
|
2805
|
+
|
|
2806
|
+
:param List[IntegrationFinding] findings: List of pre-consolidated findings to process
|
|
2807
|
+
"""
|
|
2808
|
+
if not findings:
|
|
2809
|
+
return
|
|
2810
|
+
|
|
2811
|
+
issues_processed = 0
|
|
2812
|
+
|
|
2813
|
+
for finding in findings:
|
|
2814
|
+
try:
|
|
2815
|
+
control_id = self._normalize_control_id_string(finding.rule_id) or finding.rule_id
|
|
2816
|
+
|
|
2817
|
+
# Create issue title
|
|
2818
|
+
issue_title = self.get_issue_title(finding)
|
|
2819
|
+
|
|
2820
|
+
# Create issue directly from the consolidated finding
|
|
2821
|
+
issue = self.create_or_update_issue_from_finding(title=issue_title, finding=finding)
|
|
2822
|
+
if issue:
|
|
2823
|
+
issues_processed += 1
|
|
2824
|
+
|
|
2825
|
+
else:
|
|
2826
|
+
logger.debug(
|
|
2827
|
+
f"Failed to create issue for control {control_id} - create_or_update_issue_from_finding returned None"
|
|
2828
|
+
)
|
|
2829
|
+
|
|
2830
|
+
except Exception as e:
|
|
2831
|
+
logger.error(f"Error processing consolidated issue for control {control_id}: {e}")
|
|
2832
|
+
|
|
2833
|
+
# Store the count for summary reporting
|
|
2834
|
+
self._issues_processed_count = issues_processed
|
|
2835
|
+
|
|
2836
|
+
def _find_existing_issue_for_control(self) -> Optional["regscale_models.Issue"]:
|
|
2837
|
+
"""
|
|
2838
|
+
Find existing issue for a specific control.
|
|
2839
|
+
|
|
2840
|
+
:param str control_id: Control ID to search for
|
|
2841
|
+
:return: Existing issue if found
|
|
2842
|
+
:rtype: Optional[regscale_models.Issue]
|
|
2843
|
+
"""
|
|
2844
|
+
# This is a simplified check - in practice you might want to search by external_id or other fields
|
|
2845
|
+
# that uniquely identify control-specific issues
|
|
2846
|
+
return None # For now, always create new issues
|
|
2847
|
+
|
|
2848
|
+
def sync_compliance(self, *args, **kwargs) -> None:
|
|
2849
|
+
"""Override sync to use consolidated issue processing and add summary reporting."""
|
|
2850
|
+
# Initialize issue counter
|
|
2851
|
+
self._issues_created_count = 0
|
|
2852
|
+
|
|
2853
|
+
try:
|
|
2854
|
+
# Initialize cache dictionaries if not already initialized
|
|
2855
|
+
if not hasattr(self, "_impl_id_by_control"):
|
|
2856
|
+
self._impl_id_by_control = {}
|
|
2857
|
+
if not hasattr(self, "_assessment_by_impl_today"):
|
|
2858
|
+
self._assessment_by_impl_today = {}
|
|
2859
|
+
|
|
2860
|
+
# Ensure existing records cache is loaded before processing
|
|
2861
|
+
self._load_existing_records_cache()
|
|
2862
|
+
|
|
2863
|
+
# CRITICAL: Pre-populate control implementation cache before any processing
|
|
2864
|
+
logger.info("🎯 Pre-populating control implementation cache for reliable issue linking...")
|
|
2865
|
+
self._populate_control_implementation_cache()
|
|
2866
|
+
|
|
2867
|
+
# Call parent's compliance data processing (assessments, etc.) but skip issue creation
|
|
2868
|
+
original_create_issues = self.create_issues
|
|
2869
|
+
self.create_issues = False # Disable base class issue creation
|
|
2870
|
+
super().sync_compliance() # Call the base ComplianceIntegration.sync_compliance method
|
|
2871
|
+
self.create_issues = original_create_issues # Restore setting
|
|
2872
|
+
|
|
2873
|
+
# CRITICAL: Copy assessments from base class cache to our cache so IssueFieldSetter can find them
|
|
2874
|
+
self._sync_assessment_cache_from_base_class()
|
|
2875
|
+
|
|
2876
|
+
# Now handle issue creation with consolidated logic
|
|
2877
|
+
if self.create_issues:
|
|
2878
|
+
findings = list(self.fetch_findings())
|
|
2879
|
+
if findings:
|
|
2880
|
+
self._process_consolidated_issues(findings)
|
|
2881
|
+
|
|
2882
|
+
# Provide concise summary
|
|
2883
|
+
issues_processed = getattr(self, "_issues_processed_count", 0)
|
|
2884
|
+
|
|
2885
|
+
if issues_processed > 0:
|
|
2886
|
+
# Count actual unique issues in the database for this security plan
|
|
2887
|
+
from regscale.models import regscale_models
|
|
2888
|
+
|
|
2889
|
+
actual_issues = len(
|
|
2890
|
+
regscale_models.Issue.get_all_by_parent(parent_id=self.plan_id, parent_module=self.parent_module)
|
|
2891
|
+
)
|
|
2892
|
+
|
|
2893
|
+
logger.info(
|
|
2894
|
+
f"📊 SUMMARY: Processed {issues_processed} policy violations resulting in {actual_issues} consolidated issues for failed controls for assets in RegScale"
|
|
2895
|
+
)
|
|
2896
|
+
else:
|
|
2897
|
+
logger.info("📊 SUMMARY: No issues processed - no failed controls with existing assets")
|
|
2898
|
+
|
|
1762
2899
|
except Exception as e:
|
|
1763
|
-
|
|
2900
|
+
error_and_exit(f"Error during Wiz compliance sync: {e}")
|
|
2901
|
+
|
|
2902
|
+
def _get_regscale_asset_identifier(self, compliance_item: "WizComplianceItem") -> str:
|
|
2903
|
+
"""
|
|
2904
|
+
Get the appropriate RegScale asset identifier for a compliance item.
|
|
2905
|
+
|
|
2906
|
+
For Wiz integrations, the asset_identifier_field is "wizId", so we need to return
|
|
2907
|
+
the Wiz resource ID that will match what's stored in the RegScale Asset's wizId field.
|
|
2908
|
+
|
|
2909
|
+
:param WizComplianceItem compliance_item: Compliance item with resource information
|
|
2910
|
+
:return: Wiz resource ID that matches the RegScale Asset's wizId field
|
|
2911
|
+
:rtype: str
|
|
2912
|
+
"""
|
|
2913
|
+
resource_id = getattr(compliance_item, "resource_id", "")
|
|
2914
|
+
resource_name = getattr(compliance_item, "resource_name", "")
|
|
2915
|
+
|
|
2916
|
+
# For Wiz policy compliance, the asset identifier should be the Wiz resource ID
|
|
2917
|
+
# because that's what gets stored in RegScale Asset's wizId field (asset_identifier_field = "wizId")
|
|
2918
|
+
if resource_id:
|
|
2919
|
+
return resource_id
|
|
2920
|
+
|
|
2921
|
+
# Fallback (should not normally happen since resource_id is required)
|
|
2922
|
+
return resource_name or "Unknown Resource"
|
|
2923
|
+
|
|
2924
|
+
def _create_consolidated_asset_identifier(self, asset_mappings: Dict[str, Dict[str, str]]) -> str:
|
|
2925
|
+
"""
|
|
2926
|
+
Create a consolidated asset identifier with only asset names (one per line).
|
|
2927
|
+
|
|
2928
|
+
Format: "Asset Name 1\nAsset Name 2\nAsset Name 3"
|
|
2929
|
+
This format provides clean, human-readable asset names for POAMs and issues
|
|
2930
|
+
without cluttering them with Wiz resource IDs.
|
|
2931
|
+
|
|
2932
|
+
:param Dict[str, Dict[str, str]] asset_mappings: Map of Wiz resource IDs to asset info
|
|
2933
|
+
:return: Consolidated identifier string with asset names only
|
|
2934
|
+
:rtype: str
|
|
2935
|
+
"""
|
|
2936
|
+
if not asset_mappings:
|
|
2937
|
+
return ""
|
|
2938
|
+
|
|
2939
|
+
# Create entries that show only asset names (one per line)
|
|
2940
|
+
identifier_parts = []
|
|
2941
|
+
# Sort by asset name for consistent ordering
|
|
2942
|
+
sorted_mappings = sorted(asset_mappings.items(), key=lambda x: x[1]["name"])
|
|
2943
|
+
for wiz_id, asset_info in sorted_mappings:
|
|
2944
|
+
asset_name = asset_info["name"]
|
|
2945
|
+
wiz_resource_id = asset_info["wiz_id"]
|
|
2946
|
+
|
|
2947
|
+
# Format: Just the asset name (no Wiz resource ID for cleaner POAMs)
|
|
2948
|
+
if asset_name != wiz_resource_id:
|
|
2949
|
+
# Asset was successfully mapped, show only the name
|
|
2950
|
+
identifier_part = asset_name
|
|
2951
|
+
else:
|
|
2952
|
+
# Asset lookup failed, use the Wiz resource ID as fallback
|
|
2953
|
+
identifier_part = wiz_resource_id
|
|
2954
|
+
|
|
2955
|
+
identifier_parts.append(identifier_part)
|
|
2956
|
+
|
|
2957
|
+
# Join with newlines for multi-asset issues
|
|
2958
|
+
consolidated_identifier = "\n".join(identifier_parts)
|
|
2959
|
+
logger.debug(
|
|
2960
|
+
f"Created consolidated asset identifier with {len(identifier_parts)} assets: {consolidated_identifier}"
|
|
2961
|
+
)
|
|
2962
|
+
return consolidated_identifier
|
|
1764
2963
|
|
|
1765
2964
|
|
|
1766
2965
|
def resolve_framework_id(framework_input: str) -> str:
|