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
|
@@ -37,7 +37,6 @@ from regscale.integrations.scanner_integration import (
|
|
|
37
37
|
ScannerIntegrationType,
|
|
38
38
|
IntegrationAsset,
|
|
39
39
|
IntegrationFinding,
|
|
40
|
-
issue_due_date,
|
|
41
40
|
)
|
|
42
41
|
from regscale.integrations.variables import ScannerVariables
|
|
43
42
|
from regscale.models import regscale_models
|
|
@@ -66,12 +65,18 @@ SAFE_CONTROL_ID_RE = re.compile( # NOSONAR
|
|
|
66
65
|
class WizComplianceItem(ComplianceItem):
|
|
67
66
|
"""Wiz implementation of ComplianceItem."""
|
|
68
67
|
|
|
69
|
-
def __init__(
|
|
68
|
+
def __init__(
|
|
69
|
+
self,
|
|
70
|
+
raw_data: Dict[str, Any],
|
|
71
|
+
integration: Optional["WizPolicyComplianceIntegration"] = None,
|
|
72
|
+
specific_control_id: Optional[str] = None,
|
|
73
|
+
):
|
|
70
74
|
"""
|
|
71
75
|
Initialize WizComplianceItem from raw GraphQL response.
|
|
72
76
|
|
|
73
77
|
:param Dict[str, Any] raw_data: Raw policy assessment data from Wiz
|
|
74
78
|
:param Optional['WizPolicyComplianceIntegration'] integration: Integration instance for framework mapping
|
|
79
|
+
:param Optional[str] specific_control_id: Specific control ID to use (for multi-control policies)
|
|
75
80
|
"""
|
|
76
81
|
self.id = raw_data.get("id", "")
|
|
77
82
|
self.result = raw_data.get("result", "")
|
|
@@ -79,6 +84,7 @@ class WizComplianceItem(ComplianceItem):
|
|
|
79
84
|
self.resource = raw_data.get("resource", {})
|
|
80
85
|
self.output = raw_data.get("output", {})
|
|
81
86
|
self._integration = integration
|
|
87
|
+
self._specific_control_id = specific_control_id
|
|
82
88
|
|
|
83
89
|
def _get_filtered_subcategories(self) -> List[Dict[str, Any]]:
|
|
84
90
|
"""
|
|
@@ -110,15 +116,24 @@ class WizComplianceItem(ComplianceItem):
|
|
|
110
116
|
"""Human-readable name of the resource."""
|
|
111
117
|
return self.resource.get("name", "")
|
|
112
118
|
|
|
119
|
+
@property
|
|
120
|
+
def provider_unique_id(self) -> str:
|
|
121
|
+
"""Provider unique ID (e.g., ARN for AWS resources) for meaningful asset identification."""
|
|
122
|
+
return self.resource.get("providerUniqueId", "")
|
|
123
|
+
|
|
113
124
|
@property
|
|
114
125
|
def control_id(self) -> str:
|
|
115
126
|
"""Control identifier (e.g., AC-3, SI-2)."""
|
|
127
|
+
# If a specific control ID was provided (for multi-control policies), use it
|
|
128
|
+
if self._specific_control_id:
|
|
129
|
+
return self._specific_control_id
|
|
130
|
+
|
|
116
131
|
if not self.policy:
|
|
117
132
|
return ""
|
|
118
133
|
|
|
119
134
|
subcategories = self._get_filtered_subcategories()
|
|
120
135
|
if subcategories:
|
|
121
|
-
return subcategories[0].get("externalId", "")
|
|
136
|
+
return subcategories[0].get("externalId", "").strip()
|
|
122
137
|
return ""
|
|
123
138
|
|
|
124
139
|
@property
|
|
@@ -285,6 +300,10 @@ class WizPolicyComplianceIntegration(ComplianceIntegration):
|
|
|
285
300
|
self._map_framework_id_to_name(framework_id),
|
|
286
301
|
)
|
|
287
302
|
|
|
303
|
+
# Configure strict control failure threshold for Wiz project-scoped assessments
|
|
304
|
+
# Since Wiz filters to project resources, use 0% failure tolerance
|
|
305
|
+
self.control_failure_threshold = 0.0
|
|
306
|
+
|
|
288
307
|
def fetch_compliance_data(self) -> List[Any]:
|
|
289
308
|
"""
|
|
290
309
|
Fetch compliance data from Wiz GraphQL API and filter to framework-specific
|
|
@@ -324,7 +343,10 @@ class WizPolicyComplianceIntegration(ComplianceIntegration):
|
|
|
324
343
|
|
|
325
344
|
def _filter_assessments_to_existing_assets(self, assessments: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
326
345
|
"""
|
|
327
|
-
Filter assessments to
|
|
346
|
+
Filter assessments to include items with control IDs and existing assets.
|
|
347
|
+
|
|
348
|
+
For compliance reporting, PASS controls are always included even without assets
|
|
349
|
+
to ensure complete compliance documentation.
|
|
328
350
|
|
|
329
351
|
:param assessments: List of raw assessments from Wiz
|
|
330
352
|
:return: Filtered list of assessments
|
|
@@ -343,26 +365,187 @@ class WizPolicyComplianceIntegration(ComplianceIntegration):
|
|
|
343
365
|
skipped_no_control += 1
|
|
344
366
|
continue
|
|
345
367
|
|
|
346
|
-
#
|
|
368
|
+
# For PASS controls, allow through even without existing assets for compliance documentation
|
|
369
|
+
is_pass = temp_item.compliance_result in self.PASS_STATUSES
|
|
370
|
+
|
|
371
|
+
# Skip if asset doesn't exist in RegScale UNLESS it's a PASS control
|
|
347
372
|
if temp_item.resource_id not in assets_exist:
|
|
348
|
-
|
|
349
|
-
|
|
373
|
+
if not is_pass:
|
|
374
|
+
skipped_no_asset += 1
|
|
375
|
+
continue
|
|
376
|
+
# PASS control without asset - allow through for compliance documentation
|
|
350
377
|
|
|
351
378
|
filtered_assessments.append(assessment)
|
|
352
379
|
logger.debug(f"Skipped {skipped_no_control} assessments with no control ID for framework.")
|
|
353
|
-
logger.debug(
|
|
380
|
+
logger.debug(
|
|
381
|
+
f"Skipped {skipped_no_asset} assessments with no existing asset in RegScale (PASS controls allowed)."
|
|
382
|
+
)
|
|
354
383
|
return filtered_assessments
|
|
355
384
|
|
|
356
385
|
def create_compliance_item(self, raw_data: Any) -> ComplianceItem:
|
|
357
386
|
"""
|
|
358
387
|
Create a ComplianceItem from raw compliance data.
|
|
359
388
|
|
|
389
|
+
Note: This creates a single item for the first control ID only.
|
|
390
|
+
Use create_all_compliance_items() to get all control mappings.
|
|
391
|
+
|
|
360
392
|
:param Any raw_data: Raw compliance data from Wiz
|
|
361
393
|
:return: ComplianceItem instance
|
|
362
394
|
:rtype: ComplianceItem
|
|
363
395
|
"""
|
|
364
396
|
return WizComplianceItem(raw_data, self)
|
|
365
397
|
|
|
398
|
+
def create_all_compliance_items(self, raw_data: Any) -> List[ComplianceItem]:
|
|
399
|
+
"""
|
|
400
|
+
Create all ComplianceItems from raw compliance data.
|
|
401
|
+
|
|
402
|
+
This handles Wiz policies that map to multiple controls by creating
|
|
403
|
+
a separate ComplianceItem for each control ID.
|
|
404
|
+
|
|
405
|
+
:param Any raw_data: Raw compliance data from Wiz
|
|
406
|
+
:return: List of ComplianceItem instances (one per control)
|
|
407
|
+
:rtype: List[ComplianceItem]
|
|
408
|
+
"""
|
|
409
|
+
# First get all control IDs this policy maps to
|
|
410
|
+
temp_item = WizComplianceItem(raw_data, self)
|
|
411
|
+
all_control_ids = self._get_all_control_ids_for_compliance_item(temp_item)
|
|
412
|
+
|
|
413
|
+
if not all_control_ids:
|
|
414
|
+
# No control IDs found, return single item with default behavior
|
|
415
|
+
return [temp_item]
|
|
416
|
+
|
|
417
|
+
# Create one compliance item per control ID
|
|
418
|
+
compliance_items = []
|
|
419
|
+
for control_id in all_control_ids:
|
|
420
|
+
compliance_items.append(WizComplianceItem(raw_data, self, specific_control_id=control_id))
|
|
421
|
+
|
|
422
|
+
return compliance_items
|
|
423
|
+
|
|
424
|
+
def process_compliance_data(self) -> None:
|
|
425
|
+
"""
|
|
426
|
+
Override base class to handle multi-control Wiz policies.
|
|
427
|
+
|
|
428
|
+
Creates separate compliance items for each control ID that a policy maps to.
|
|
429
|
+
"""
|
|
430
|
+
logger.info("Processing compliance data with multi-control support...")
|
|
431
|
+
|
|
432
|
+
# Reset state to avoid double counting on repeated calls
|
|
433
|
+
self._reset_compliance_state()
|
|
434
|
+
|
|
435
|
+
# Build allowed control IDs from plan/catalog controls to restrict scope
|
|
436
|
+
allowed_controls_normalized = self._build_allowed_controls_set()
|
|
437
|
+
|
|
438
|
+
# Fetch and process raw compliance data
|
|
439
|
+
raw_compliance_data = self.fetch_compliance_data()
|
|
440
|
+
total_policies_processed, total_compliance_items_created = self._process_raw_compliance_data(
|
|
441
|
+
raw_compliance_data, allowed_controls_normalized
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
# Perform control-level categorization based on aggregated results
|
|
445
|
+
self._categorize_controls_by_aggregation()
|
|
446
|
+
|
|
447
|
+
self._log_processing_summary(total_policies_processed, total_compliance_items_created)
|
|
448
|
+
|
|
449
|
+
def _reset_compliance_state(self) -> None:
|
|
450
|
+
"""Reset state to avoid double counting on repeated calls."""
|
|
451
|
+
self.all_compliance_items = []
|
|
452
|
+
self.failed_compliance_items = []
|
|
453
|
+
self.passing_controls = {}
|
|
454
|
+
self.failing_controls = {}
|
|
455
|
+
self.asset_compliance_map.clear()
|
|
456
|
+
|
|
457
|
+
def _build_allowed_controls_set(self) -> set[str]:
|
|
458
|
+
"""Build allowed control IDs from plan/catalog controls to restrict scope."""
|
|
459
|
+
allowed_controls_normalized: set[str] = set()
|
|
460
|
+
try:
|
|
461
|
+
controls = self._get_controls()
|
|
462
|
+
for ctl in controls:
|
|
463
|
+
cid = (ctl.get("controlId") or "").strip()
|
|
464
|
+
if not cid:
|
|
465
|
+
continue
|
|
466
|
+
base, sub = self._normalize_control_id(cid)
|
|
467
|
+
normalized = f"{base}({sub})" if sub else base
|
|
468
|
+
allowed_controls_normalized.add(normalized)
|
|
469
|
+
except Exception:
|
|
470
|
+
# If controls cannot be loaded, proceed without additional filtering
|
|
471
|
+
allowed_controls_normalized = set()
|
|
472
|
+
return allowed_controls_normalized
|
|
473
|
+
|
|
474
|
+
def _process_raw_compliance_data(
|
|
475
|
+
self, raw_compliance_data: List[Any], allowed_controls_normalized: set[str]
|
|
476
|
+
) -> tuple[int, int]:
|
|
477
|
+
"""Process raw compliance data and return counts."""
|
|
478
|
+
total_policies_processed = 0
|
|
479
|
+
total_compliance_items_created = 0
|
|
480
|
+
|
|
481
|
+
for raw_item in raw_compliance_data:
|
|
482
|
+
try:
|
|
483
|
+
total_policies_processed += 1
|
|
484
|
+
compliance_items_for_policy = self.create_all_compliance_items(raw_item)
|
|
485
|
+
|
|
486
|
+
items_created_for_policy = self._process_compliance_items_for_policy(
|
|
487
|
+
compliance_items_for_policy, allowed_controls_normalized
|
|
488
|
+
)
|
|
489
|
+
total_compliance_items_created += items_created_for_policy
|
|
490
|
+
|
|
491
|
+
except Exception as e:
|
|
492
|
+
logger.error(f"Error processing compliance item: {e}")
|
|
493
|
+
continue
|
|
494
|
+
|
|
495
|
+
return total_policies_processed, total_compliance_items_created
|
|
496
|
+
|
|
497
|
+
def _process_compliance_items_for_policy(
|
|
498
|
+
self, compliance_items_for_policy: List[Any], allowed_controls_normalized: set[str]
|
|
499
|
+
) -> int:
|
|
500
|
+
"""Process compliance items for a single policy and return count of items created."""
|
|
501
|
+
items_created = 0
|
|
502
|
+
|
|
503
|
+
for compliance_item in compliance_items_for_policy:
|
|
504
|
+
if not self._is_valid_compliance_item(compliance_item):
|
|
505
|
+
continue
|
|
506
|
+
|
|
507
|
+
if not self._is_control_in_allowed_set(compliance_item, allowed_controls_normalized):
|
|
508
|
+
continue
|
|
509
|
+
|
|
510
|
+
self._add_compliance_item_to_collections(compliance_item)
|
|
511
|
+
items_created += 1
|
|
512
|
+
|
|
513
|
+
return items_created
|
|
514
|
+
|
|
515
|
+
def _is_valid_compliance_item(self, compliance_item: Any) -> bool:
|
|
516
|
+
"""Check if compliance item has required control_id and resource_id."""
|
|
517
|
+
return getattr(compliance_item, "control_id", "") and getattr(compliance_item, "resource_id", "")
|
|
518
|
+
|
|
519
|
+
def _is_control_in_allowed_set(self, compliance_item: Any, allowed_controls_normalized: set[str]) -> bool:
|
|
520
|
+
"""Check if compliance item's control is in allowed set."""
|
|
521
|
+
if not allowed_controls_normalized:
|
|
522
|
+
return True
|
|
523
|
+
|
|
524
|
+
base, sub = self._normalize_control_id(getattr(compliance_item, "control_id", ""))
|
|
525
|
+
norm_item = f"{base}({sub})" if sub else base
|
|
526
|
+
return norm_item in allowed_controls_normalized
|
|
527
|
+
|
|
528
|
+
def _add_compliance_item_to_collections(self, compliance_item: Any) -> None:
|
|
529
|
+
"""Add compliance item to appropriate collections."""
|
|
530
|
+
self.all_compliance_items.append(compliance_item)
|
|
531
|
+
self.asset_compliance_map[compliance_item.resource_id].append(compliance_item)
|
|
532
|
+
|
|
533
|
+
if compliance_item.compliance_result in self.FAIL_STATUSES:
|
|
534
|
+
self.failed_compliance_items.append(compliance_item)
|
|
535
|
+
|
|
536
|
+
def _log_processing_summary(self, total_policies_processed: int, total_compliance_items_created: int) -> None:
|
|
537
|
+
"""Log processing summary information."""
|
|
538
|
+
logger.info(
|
|
539
|
+
f"Processed {total_policies_processed} Wiz policies into {total_compliance_items_created} compliance items"
|
|
540
|
+
)
|
|
541
|
+
logger.debug(
|
|
542
|
+
f"Compliance breakdown: {len(self.all_compliance_items) - len(self.failed_compliance_items)} passing items, "
|
|
543
|
+
f"{len(self.failed_compliance_items)} failing items"
|
|
544
|
+
)
|
|
545
|
+
logger.info(
|
|
546
|
+
f"Control categorization: {len(self.passing_controls)} passing controls, {len(self.failing_controls)} failing controls"
|
|
547
|
+
)
|
|
548
|
+
|
|
366
549
|
def _map_resource_type_to_asset_type(self, compliance_item: ComplianceItem) -> str:
|
|
367
550
|
"""
|
|
368
551
|
Map Wiz resource type to RegScale asset type.
|
|
@@ -442,7 +625,7 @@ class WizPolicyComplianceIntegration(ComplianceIntegration):
|
|
|
442
625
|
seen = set()
|
|
443
626
|
|
|
444
627
|
for subcat in subcategories:
|
|
445
|
-
external_id = subcat.get("externalId", "")
|
|
628
|
+
external_id = subcat.get("externalId", "").strip()
|
|
446
629
|
if external_id and external_id not in seen:
|
|
447
630
|
seen.add(external_id)
|
|
448
631
|
unique_control_ids.append(external_id)
|
|
@@ -496,7 +679,6 @@ class WizPolicyComplianceIntegration(ComplianceIntegration):
|
|
|
496
679
|
:rtype: Iterator[IntegrationFinding]
|
|
497
680
|
"""
|
|
498
681
|
for control_id, resources in control_to_resources.items():
|
|
499
|
-
|
|
500
682
|
# Use the first compliance item as the base for this control's finding
|
|
501
683
|
base_compliance_item = next(iter(resources.values()))
|
|
502
684
|
|
|
@@ -826,6 +1008,7 @@ class WizPolicyComplianceIntegration(ComplianceIntegration):
|
|
|
826
1008
|
last_seen=self.scan_date,
|
|
827
1009
|
scan_date=self.scan_date,
|
|
828
1010
|
asset_identifier=self._get_regscale_asset_identifier(compliance_item),
|
|
1011
|
+
issue_asset_identifier_value=self._get_provider_unique_id_for_asset_identifier(compliance_item),
|
|
829
1012
|
vulnerability_type="Policy Compliance Violation",
|
|
830
1013
|
rule_id=compliance_item.control_id,
|
|
831
1014
|
baseline=compliance_item.framework,
|
|
@@ -1024,36 +1207,33 @@ class WizPolicyComplianceIntegration(ComplianceIntegration):
|
|
|
1024
1207
|
"""
|
|
1025
1208
|
logger.info("Fetching policy assessments from Wiz...")
|
|
1026
1209
|
|
|
1027
|
-
# Authenticate if not already done
|
|
1028
1210
|
if not self.access_token:
|
|
1029
1211
|
self.authenticate_wiz()
|
|
1030
1212
|
|
|
1031
|
-
headers = self._build_wiz_headers()
|
|
1032
|
-
session = self._prepare_wiz_requests_session()
|
|
1033
|
-
|
|
1034
|
-
# Try cache first unless forced refresh
|
|
1035
1213
|
cached_nodes = self._load_assessments_from_cache()
|
|
1036
1214
|
if cached_nodes is not None:
|
|
1037
1215
|
logger.info("Using cached Wiz policy assessments")
|
|
1038
1216
|
return cached_nodes
|
|
1039
1217
|
|
|
1040
|
-
#
|
|
1041
|
-
|
|
1042
|
-
|
|
1218
|
+
# Try async approach first
|
|
1219
|
+
async_results = self._try_async_assessment_fetch()
|
|
1220
|
+
if async_results is not None:
|
|
1221
|
+
self._write_assessments_cache(async_results)
|
|
1222
|
+
return async_results
|
|
1043
1223
|
|
|
1044
|
-
#
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
{"projects": [self.wiz_project_id]},
|
|
1049
|
-
{}, # Empty filterBy
|
|
1050
|
-
None, # Omit filterBy entirely
|
|
1051
|
-
]
|
|
1224
|
+
# Fall back to requests-based method
|
|
1225
|
+
filtered_nodes = self._fetch_assessments_with_requests()
|
|
1226
|
+
self._write_assessments_cache(filtered_nodes)
|
|
1227
|
+
return filtered_nodes
|
|
1052
1228
|
|
|
1053
|
-
|
|
1229
|
+
def _try_async_assessment_fetch(self) -> Optional[List[Dict[str, Any]]]:
|
|
1230
|
+
"""Try to fetch assessments using async client."""
|
|
1054
1231
|
try:
|
|
1055
1232
|
from regscale.integrations.commercial.wizv2.utils import compliance_job_progress
|
|
1056
1233
|
|
|
1234
|
+
page_size = 100
|
|
1235
|
+
headers = self._build_wiz_headers()
|
|
1236
|
+
|
|
1057
1237
|
with compliance_job_progress:
|
|
1058
1238
|
task = compliance_job_progress.add_task(
|
|
1059
1239
|
f"[#f68d1f]Fetching Wiz policy assessments (async, page size: {page_size})...",
|
|
@@ -1074,24 +1254,36 @@ class WizPolicyComplianceIntegration(ComplianceIntegration):
|
|
|
1074
1254
|
max_concurrent=1,
|
|
1075
1255
|
)
|
|
1076
1256
|
compliance_job_progress.update(task, completed=1, advance=1)
|
|
1257
|
+
|
|
1077
1258
|
if results and len(results) == 1 and not results[0][2]:
|
|
1078
1259
|
nodes = results[0][1] or []
|
|
1079
|
-
|
|
1080
|
-
self._write_assessments_cache(filtered)
|
|
1081
|
-
return filtered
|
|
1260
|
+
return self._filter_nodes_to_framework(nodes)
|
|
1082
1261
|
except Exception:
|
|
1083
|
-
# Fall back to requests-based method below
|
|
1084
1262
|
pass
|
|
1263
|
+
return None
|
|
1085
1264
|
|
|
1086
|
-
|
|
1265
|
+
def _fetch_assessments_with_requests(self) -> List[Dict[str, Any]]:
|
|
1266
|
+
"""Fetch assessments using requests-based method with filter variants."""
|
|
1267
|
+
headers = self._build_wiz_headers()
|
|
1268
|
+
session = self._prepare_wiz_requests_session()
|
|
1269
|
+
page_size = 100
|
|
1270
|
+
base_variables = {"first": page_size}
|
|
1271
|
+
|
|
1272
|
+
filter_variants = [
|
|
1273
|
+
{"project": [self.wiz_project_id]},
|
|
1274
|
+
{"projectId": [self.wiz_project_id]},
|
|
1275
|
+
{"projects": [self.wiz_project_id]},
|
|
1276
|
+
{}, # Empty filterBy
|
|
1277
|
+
None, # Omit filterBy entirely
|
|
1278
|
+
]
|
|
1279
|
+
|
|
1280
|
+
return self._fetch_assessments_with_variants(
|
|
1087
1281
|
session=session,
|
|
1088
1282
|
headers=headers,
|
|
1089
1283
|
base_variables=base_variables,
|
|
1090
1284
|
page_size=page_size,
|
|
1091
1285
|
filter_variants=filter_variants,
|
|
1092
1286
|
)
|
|
1093
|
-
self._write_assessments_cache(filtered_nodes)
|
|
1094
|
-
return filtered_nodes
|
|
1095
1287
|
|
|
1096
1288
|
def _build_wiz_headers(self) -> Dict[str, str]:
|
|
1097
1289
|
"""
|
|
@@ -1127,7 +1319,7 @@ class WizPolicyComplianceIntegration(ComplianceIntegration):
|
|
|
1127
1319
|
)
|
|
1128
1320
|
adapter = HTTPAdapter(max_retries=retry)
|
|
1129
1321
|
session.mount("https://", adapter)
|
|
1130
|
-
session.mount("http://", adapter)
|
|
1322
|
+
session.mount("http://", adapter) # NO SONAR #NOSONAR
|
|
1131
1323
|
return session
|
|
1132
1324
|
|
|
1133
1325
|
def _fetch_assessments_with_variants(
|
|
@@ -1377,19 +1569,70 @@ class WizPolicyComplianceIntegration(ComplianceIntegration):
|
|
|
1377
1569
|
:return: Path to the written JSON file
|
|
1378
1570
|
:rtype: str
|
|
1379
1571
|
"""
|
|
1380
|
-
#
|
|
1572
|
+
# Setup file paths
|
|
1573
|
+
artifacts_dir, timestamp, file_path, file_path_jsonl = self._setup_output_files()
|
|
1574
|
+
|
|
1575
|
+
# Build compliance summary data
|
|
1576
|
+
catalog_controls = self._get_catalog_controls()
|
|
1577
|
+
control_sets = self._build_control_sets(catalog_controls)
|
|
1578
|
+
|
|
1579
|
+
# Prepare export data structure
|
|
1580
|
+
export_data = self._build_export_data(timestamp, catalog_controls, control_sets)
|
|
1581
|
+
|
|
1582
|
+
# Convert compliance items to serializable format
|
|
1583
|
+
self._add_policy_assessments_to_export(export_data)
|
|
1584
|
+
|
|
1585
|
+
# Write files and cleanup
|
|
1586
|
+
return self._write_output_files(file_path, file_path_jsonl, export_data, artifacts_dir)
|
|
1587
|
+
|
|
1588
|
+
def _setup_output_files(self) -> tuple[str, str, str, str]:
|
|
1589
|
+
"""Setup output directory and file paths."""
|
|
1381
1590
|
artifacts_dir = os.path.join("artifacts", "wiz")
|
|
1382
1591
|
os.makedirs(artifacts_dir, exist_ok=True)
|
|
1383
1592
|
|
|
1384
|
-
# Generate timestamped filename
|
|
1385
1593
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
1386
1594
|
filename_json = f"policy_compliance_report_{timestamp}.json"
|
|
1387
1595
|
filename_jsonl = f"policy_compliance_report_{timestamp}.jsonl"
|
|
1388
1596
|
file_path = os.path.join(artifacts_dir, filename_json)
|
|
1389
1597
|
file_path_jsonl = os.path.join(artifacts_dir, filename_jsonl)
|
|
1390
1598
|
|
|
1391
|
-
|
|
1392
|
-
|
|
1599
|
+
return artifacts_dir, timestamp, file_path, file_path_jsonl
|
|
1600
|
+
|
|
1601
|
+
def _get_catalog_controls(self) -> set[str]:
|
|
1602
|
+
"""Get catalog controls from the plan/catalog."""
|
|
1603
|
+
catalog_controls = set()
|
|
1604
|
+
try:
|
|
1605
|
+
controls = self._get_controls()
|
|
1606
|
+
for ctl in controls:
|
|
1607
|
+
cid = (ctl.get("controlId") or "").strip()
|
|
1608
|
+
if cid:
|
|
1609
|
+
catalog_controls.add(cid)
|
|
1610
|
+
except Exception:
|
|
1611
|
+
catalog_controls = set()
|
|
1612
|
+
return catalog_controls
|
|
1613
|
+
|
|
1614
|
+
def _build_control_sets(self, catalog_controls: set[str]) -> Dict[str, set]:
|
|
1615
|
+
"""Build control sets for summary calculations."""
|
|
1616
|
+
assessed_controls = {item.control_id for item in self.all_compliance_items if item.control_id}
|
|
1617
|
+
passing_control_ids = {key.upper() for key in self.passing_controls.keys()}
|
|
1618
|
+
failing_control_ids = {key.upper() for key in self.failing_controls.keys()}
|
|
1619
|
+
|
|
1620
|
+
return {
|
|
1621
|
+
"assessed": assessed_controls,
|
|
1622
|
+
"passing": passing_control_ids,
|
|
1623
|
+
"failing": failing_control_ids,
|
|
1624
|
+
"catalog": catalog_controls,
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
def _build_export_data(
|
|
1628
|
+
self, timestamp: str, catalog_controls: set[str], control_sets: Dict[str, set]
|
|
1629
|
+
) -> Dict[str, Any]:
|
|
1630
|
+
"""Build the main export data structure."""
|
|
1631
|
+
assessed_controls = control_sets["assessed"]
|
|
1632
|
+
passing_control_ids = control_sets["passing"]
|
|
1633
|
+
failing_control_ids = control_sets["failing"]
|
|
1634
|
+
|
|
1635
|
+
return {
|
|
1393
1636
|
"metadata": {
|
|
1394
1637
|
"timestamp": timestamp,
|
|
1395
1638
|
"wiz_project_id": self.wiz_project_id,
|
|
@@ -1398,62 +1641,107 @@ class WizPolicyComplianceIntegration(ComplianceIntegration):
|
|
|
1398
1641
|
"total_assessments": len(self.all_compliance_items),
|
|
1399
1642
|
"pass_count": len(self.all_compliance_items) - len(self.failed_compliance_items),
|
|
1400
1643
|
"fail_count": len(self.failed_compliance_items),
|
|
1401
|
-
"unique_controls": len(
|
|
1644
|
+
"unique_controls": len(assessed_controls),
|
|
1645
|
+
"catalog_summary": self._build_catalog_summary(
|
|
1646
|
+
catalog_controls, assessed_controls, passing_control_ids, failing_control_ids
|
|
1647
|
+
),
|
|
1402
1648
|
},
|
|
1403
1649
|
"framework_mapping": self.framework_mapping,
|
|
1650
|
+
"control_summary": {
|
|
1651
|
+
"passing_controls": list(passing_control_ids),
|
|
1652
|
+
"failing_controls": list(failing_control_ids),
|
|
1653
|
+
"catalog_controls_no_wiz_data": list(catalog_controls - assessed_controls - passing_control_ids),
|
|
1654
|
+
"wiz_controls_outside_catalog": list(assessed_controls - catalog_controls),
|
|
1655
|
+
},
|
|
1404
1656
|
"policy_assessments": [],
|
|
1405
1657
|
}
|
|
1406
1658
|
|
|
1407
|
-
|
|
1659
|
+
def _build_catalog_summary(
|
|
1660
|
+
self,
|
|
1661
|
+
catalog_controls: set[str],
|
|
1662
|
+
assessed_controls: set[str],
|
|
1663
|
+
passing_control_ids: set[str],
|
|
1664
|
+
failing_control_ids: set[str],
|
|
1665
|
+
) -> Dict[str, int]:
|
|
1666
|
+
"""Build catalog summary statistics."""
|
|
1667
|
+
return {
|
|
1668
|
+
"total_catalog_controls": len(catalog_controls),
|
|
1669
|
+
"catalog_controls_with_wiz_data": len(catalog_controls.intersection(assessed_controls)),
|
|
1670
|
+
"catalog_controls_passing": len(catalog_controls.intersection(passing_control_ids)),
|
|
1671
|
+
"catalog_controls_failing": len(catalog_controls.intersection(failing_control_ids)),
|
|
1672
|
+
"catalog_controls_no_data": len(catalog_controls - assessed_controls - passing_control_ids),
|
|
1673
|
+
"wiz_controls_outside_catalog": len(assessed_controls - catalog_controls),
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
def _add_policy_assessments_to_export(self, export_data: Dict[str, Any]) -> None:
|
|
1677
|
+
"""Add policy assessments to export data."""
|
|
1408
1678
|
for compliance_item in self.all_compliance_items:
|
|
1409
1679
|
if isinstance(compliance_item, WizComplianceItem):
|
|
1410
|
-
|
|
1411
|
-
filtered_policy = dict(compliance_item.policy) if compliance_item.policy else {}
|
|
1412
|
-
if filtered_policy:
|
|
1413
|
-
subcats = filtered_policy.get("securitySubCategories", [])
|
|
1414
|
-
if subcats:
|
|
1415
|
-
target_framework_id = self.framework_id
|
|
1416
|
-
filtered_subcats = [
|
|
1417
|
-
sc
|
|
1418
|
-
for sc in subcats
|
|
1419
|
-
if sc.get("category", {}).get("framework", {}).get("id") == target_framework_id
|
|
1420
|
-
]
|
|
1421
|
-
if filtered_subcats:
|
|
1422
|
-
filtered_policy["securitySubCategories"] = filtered_subcats
|
|
1423
|
-
else:
|
|
1424
|
-
# If filter removes all, keep original to retain context
|
|
1425
|
-
pass
|
|
1426
|
-
assessment_data = {
|
|
1427
|
-
"id": compliance_item.id,
|
|
1428
|
-
"result": compliance_item.result,
|
|
1429
|
-
"control_id": compliance_item.control_id,
|
|
1430
|
-
"framework_name": compliance_item.framework,
|
|
1431
|
-
"framework_id": compliance_item.framework_id,
|
|
1432
|
-
"policy": filtered_policy or compliance_item.policy,
|
|
1433
|
-
"resource": compliance_item.resource,
|
|
1434
|
-
"output": compliance_item.output,
|
|
1435
|
-
}
|
|
1680
|
+
assessment_data = self._build_assessment_data(compliance_item)
|
|
1436
1681
|
export_data["policy_assessments"].append(assessment_data)
|
|
1437
1682
|
|
|
1438
|
-
|
|
1683
|
+
def _build_assessment_data(self, compliance_item: WizComplianceItem) -> Dict[str, Any]:
|
|
1684
|
+
"""Build assessment data for a single compliance item."""
|
|
1685
|
+
filtered_policy = self._filter_policy_subcategories(compliance_item)
|
|
1686
|
+
|
|
1687
|
+
return {
|
|
1688
|
+
"id": compliance_item.id,
|
|
1689
|
+
"result": compliance_item.result,
|
|
1690
|
+
"control_id": compliance_item.control_id,
|
|
1691
|
+
"framework_name": compliance_item.framework,
|
|
1692
|
+
"framework_id": compliance_item.framework_id,
|
|
1693
|
+
"policy": filtered_policy or compliance_item.policy,
|
|
1694
|
+
"resource": compliance_item.resource,
|
|
1695
|
+
"output": compliance_item.output,
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
def _filter_policy_subcategories(self, compliance_item: WizComplianceItem) -> Dict[str, Any]:
|
|
1699
|
+
"""Filter policy subcategories to only the selected framework."""
|
|
1700
|
+
filtered_policy = dict(compliance_item.policy) if compliance_item.policy else {}
|
|
1701
|
+
if not filtered_policy:
|
|
1702
|
+
return filtered_policy
|
|
1703
|
+
|
|
1704
|
+
subcats = filtered_policy.get("securitySubCategories", [])
|
|
1705
|
+
if not subcats:
|
|
1706
|
+
return filtered_policy
|
|
1707
|
+
|
|
1708
|
+
target_framework_id = self.framework_id
|
|
1709
|
+
filtered_subcats = [
|
|
1710
|
+
sc for sc in subcats if sc.get("category", {}).get("framework", {}).get("id") == target_framework_id
|
|
1711
|
+
]
|
|
1712
|
+
|
|
1713
|
+
if filtered_subcats:
|
|
1714
|
+
filtered_policy["securitySubCategories"] = filtered_subcats
|
|
1715
|
+
|
|
1716
|
+
return filtered_policy
|
|
1717
|
+
|
|
1718
|
+
def _write_output_files(
|
|
1719
|
+
self, file_path: str, file_path_jsonl: str, export_data: Dict[str, Any], artifacts_dir: str
|
|
1720
|
+
) -> str:
|
|
1721
|
+
"""Write output files and perform cleanup."""
|
|
1439
1722
|
try:
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
logger.info(f"Policy compliance data written to: {file_path}")
|
|
1444
|
-
# JSONL: aggregated by control_id (optional)
|
|
1445
|
-
if getattr(self, "write_jsonl_output", False):
|
|
1446
|
-
control_agg = self._build_control_aggregation()
|
|
1447
|
-
with open(file_path_jsonl, "w", encoding="utf-8") as jf:
|
|
1448
|
-
for control_id, ctrl in control_agg.items():
|
|
1449
|
-
jf.write(json.dumps(ctrl, ensure_ascii=False) + "\n")
|
|
1450
|
-
logger.info(f"Policy compliance JSONL written to: {file_path_jsonl}")
|
|
1723
|
+
self._write_json_file(file_path, export_data)
|
|
1724
|
+
self._write_jsonl_file_if_enabled(file_path_jsonl)
|
|
1451
1725
|
self._cleanup_artifacts(artifacts_dir, keep=CACHE_CLEANUP_KEEP_COUNT)
|
|
1452
1726
|
return file_path
|
|
1453
|
-
|
|
1454
1727
|
except Exception as e:
|
|
1455
1728
|
error_and_exit(f"Failed to write policy data to JSON: {str(e)}")
|
|
1456
1729
|
|
|
1730
|
+
def _write_json_file(self, file_path: str, export_data: Dict[str, Any]) -> None:
|
|
1731
|
+
"""Write JSON export data to file."""
|
|
1732
|
+
with open(file_path, "w", encoding="utf-8") as f:
|
|
1733
|
+
json.dump(export_data, f, indent=2, ensure_ascii=False)
|
|
1734
|
+
logger.info(f"Policy compliance data written to: {file_path}")
|
|
1735
|
+
|
|
1736
|
+
def _write_jsonl_file_if_enabled(self, file_path_jsonl: str) -> None:
|
|
1737
|
+
"""Write JSONL file if output is enabled."""
|
|
1738
|
+
if getattr(self, "write_jsonl_output", False):
|
|
1739
|
+
control_agg = self._build_control_aggregation()
|
|
1740
|
+
with open(file_path_jsonl, "w", encoding="utf-8") as jf:
|
|
1741
|
+
for control_id, ctrl in control_agg.items():
|
|
1742
|
+
jf.write(json.dumps(ctrl, ensure_ascii=False) + "\n")
|
|
1743
|
+
logger.info(f"Policy compliance JSONL written to: {file_path_jsonl}")
|
|
1744
|
+
|
|
1457
1745
|
def _build_control_aggregation(self) -> Dict[str, Dict[str, Any]]:
|
|
1458
1746
|
"""
|
|
1459
1747
|
Build an aggregated view per control_id for JSONL export.
|
|
@@ -1975,7 +2263,7 @@ class WizPolicyComplianceIntegration(ComplianceIntegration):
|
|
|
1975
2263
|
# Update basic fields (similar to parent class logic)
|
|
1976
2264
|
existing_issue.title = title
|
|
1977
2265
|
existing_issue.description = finding.description
|
|
1978
|
-
existing_issue.
|
|
2266
|
+
existing_issue.severityLevel = finding.severity
|
|
1979
2267
|
existing_issue.status = finding.status
|
|
1980
2268
|
existing_issue.dateLastUpdated = self.scan_date
|
|
1981
2269
|
|
|
@@ -2013,24 +2301,44 @@ class WizPolicyComplianceIntegration(ComplianceIntegration):
|
|
|
2013
2301
|
|
|
2014
2302
|
CRITICAL FIX: Check if the finding has assessment parent overrides and apply them.
|
|
2015
2303
|
"""
|
|
2016
|
-
# Get consolidated asset identifier
|
|
2017
2304
|
asset_identifier = self.get_consolidated_asset_identifier(finding, existing_issue)
|
|
2018
|
-
|
|
2019
|
-
# Prepare issue data
|
|
2020
|
-
issue_title = self.get_issue_title(finding) or title
|
|
2021
|
-
description = finding.description or ""
|
|
2022
|
-
remediation_description = finding.recommendation_for_mitigation or finding.remediation or ""
|
|
2023
|
-
is_poam = self.is_poam(finding)
|
|
2305
|
+
issue_data = self._prepare_issue_data(finding, title)
|
|
2024
2306
|
|
|
2025
2307
|
if existing_issue:
|
|
2026
2308
|
logger.debug(
|
|
2027
2309
|
"Updating existing issue %s with assetIdentifier %s", existing_issue.id, finding.asset_identifier
|
|
2028
2310
|
)
|
|
2029
2311
|
|
|
2030
|
-
# If we have an existing issue, update its fields instead of creating a new one
|
|
2031
2312
|
issue = existing_issue or regscale_models.Issue()
|
|
2313
|
+
parent_info = self._get_parent_info(finding)
|
|
2032
2314
|
|
|
2033
|
-
|
|
2315
|
+
self._set_basic_issue_properties(issue, finding, issue_status, issue_data, parent_info, asset_identifier)
|
|
2316
|
+
self._set_compliance_properties(issue, finding)
|
|
2317
|
+
self._set_additional_properties(issue, finding, issue_data)
|
|
2318
|
+
|
|
2319
|
+
if finding.cve:
|
|
2320
|
+
issue = self.lookup_kev_and_update_issue(cve=finding.cve, issue=issue, cisa_kevs=self._kev_data)
|
|
2321
|
+
|
|
2322
|
+
issue = self._save_or_create_issue_record(issue, finding, existing_issue, issue_data["is_poam"])
|
|
2323
|
+
|
|
2324
|
+
if issue and issue.id:
|
|
2325
|
+
self._handle_post_creation_tasks(issue, finding, existing_issue)
|
|
2326
|
+
else:
|
|
2327
|
+
logger.debug("Skipping milestone creation - issue has no ID")
|
|
2328
|
+
|
|
2329
|
+
return issue
|
|
2330
|
+
|
|
2331
|
+
def _prepare_issue_data(self, finding: IntegrationFinding, title: str) -> Dict[str, Any]:
|
|
2332
|
+
"""Prepare basic issue data from finding."""
|
|
2333
|
+
return {
|
|
2334
|
+
"issue_title": self.get_issue_title(finding) or title,
|
|
2335
|
+
"description": finding.description or "",
|
|
2336
|
+
"remediation_description": finding.recommendation_for_mitigation or finding.remediation or "",
|
|
2337
|
+
"is_poam": self.is_poam(finding),
|
|
2338
|
+
}
|
|
2339
|
+
|
|
2340
|
+
def _get_parent_info(self, finding: IntegrationFinding) -> Dict[str, Any]:
|
|
2341
|
+
"""Get parent information for the issue."""
|
|
2034
2342
|
if hasattr(finding, "_override_parent_id") and hasattr(finding, "_override_parent_module"):
|
|
2035
2343
|
parent_id = finding._override_parent_id
|
|
2036
2344
|
parent_module = finding._override_parent_module
|
|
@@ -2039,11 +2347,22 @@ class WizPolicyComplianceIntegration(ComplianceIntegration):
|
|
|
2039
2347
|
parent_id = self.plan_id
|
|
2040
2348
|
parent_module = self.parent_module
|
|
2041
2349
|
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2350
|
+
return {"parent_id": parent_id, "parent_module": parent_module}
|
|
2351
|
+
|
|
2352
|
+
def _set_basic_issue_properties(
|
|
2353
|
+
self,
|
|
2354
|
+
issue: regscale_models.Issue,
|
|
2355
|
+
finding: IntegrationFinding,
|
|
2356
|
+
issue_status,
|
|
2357
|
+
issue_data: Dict[str, Any],
|
|
2358
|
+
parent_info: Dict[str, Any],
|
|
2359
|
+
asset_identifier: str,
|
|
2360
|
+
) -> None:
|
|
2361
|
+
"""Set basic properties on the issue."""
|
|
2362
|
+
issue.parentId = parent_info["parent_id"]
|
|
2363
|
+
issue.parentModule = parent_info["parent_module"]
|
|
2045
2364
|
issue.vulnerabilityId = finding.vulnerability_id
|
|
2046
|
-
issue.title = issue_title
|
|
2365
|
+
issue.title = issue_data["issue_title"]
|
|
2047
2366
|
issue.dateCreated = finding.date_created
|
|
2048
2367
|
issue.status = issue_status
|
|
2049
2368
|
issue.dateCompleted = (
|
|
@@ -2056,51 +2375,40 @@ class WizPolicyComplianceIntegration(ComplianceIntegration):
|
|
|
2056
2375
|
issue.securityPlanId = self.plan_id if not self.is_component else None
|
|
2057
2376
|
issue.identification = finding.identification
|
|
2058
2377
|
issue.dateFirstDetected = finding.first_seen
|
|
2059
|
-
|
|
2060
|
-
# Ensure a due date is always set using configured policy defaults (e.g., FedRAMP)
|
|
2061
|
-
if not finding.due_date:
|
|
2062
|
-
try:
|
|
2063
|
-
base_created = finding.date_created or issue.dateCreated
|
|
2064
|
-
finding.due_date = issue_due_date(
|
|
2065
|
-
severity=finding.severity,
|
|
2066
|
-
created_date=base_created,
|
|
2067
|
-
title=self.title,
|
|
2068
|
-
)
|
|
2069
|
-
except Exception:
|
|
2070
|
-
# Final fallback to a Low severity default if anything goes wrong
|
|
2071
|
-
base_created = finding.date_created or issue.dateCreated
|
|
2072
|
-
finding.due_date = issue_due_date(
|
|
2073
|
-
severity=regscale_models.IssueSeverity.Low,
|
|
2074
|
-
created_date=base_created,
|
|
2075
|
-
title=self.title,
|
|
2076
|
-
)
|
|
2077
|
-
issue.dueDate = finding.due_date
|
|
2078
|
-
issue.description = description
|
|
2079
|
-
issue.sourceReport = finding.source_report or self.title
|
|
2080
|
-
issue.recommendedActions = finding.recommendation_for_mitigation
|
|
2081
2378
|
issue.assetIdentifier = asset_identifier
|
|
2082
|
-
issue.securityChecks = finding.security_check or finding.external_id
|
|
2083
|
-
issue.remediationDescription = remediation_description
|
|
2084
|
-
issue.integrationFindingId = self.get_finding_identifier(finding)
|
|
2085
|
-
issue.poamComments = finding.poam_comments
|
|
2086
|
-
issue.cve = finding.cve
|
|
2087
2379
|
|
|
2088
|
-
#
|
|
2380
|
+
# Ensure due date is set
|
|
2381
|
+
self._set_issue_due_date(issue, finding)
|
|
2382
|
+
|
|
2383
|
+
def _set_compliance_properties(self, issue: regscale_models.Issue, finding: IntegrationFinding) -> None:
|
|
2384
|
+
"""Set compliance-specific properties."""
|
|
2089
2385
|
issue.assessmentId = finding.assessment_id
|
|
2090
|
-
logger.debug(f"SETTING assessmentId = {finding.assessment_id}
|
|
2386
|
+
logger.debug(f"SETTING assessmentId = {finding.assessment_id}")
|
|
2091
2387
|
|
|
2092
2388
|
control_id = self.get_control_implementation_id_for_cci(finding.cci_ref) if finding.cci_ref else None
|
|
2093
2389
|
issue.controlId = control_id
|
|
2094
2390
|
|
|
2095
|
-
# Add the control implementation ids and the cci ref if it exists
|
|
2096
2391
|
cci_control_ids = [control_id] if control_id is not None else []
|
|
2097
2392
|
if finding.affected_controls:
|
|
2098
2393
|
issue.affectedControls = finding.affected_controls
|
|
2099
2394
|
elif finding.control_labels:
|
|
2100
2395
|
issue.affectedControls = ", ".join(sorted({cl for cl in finding.control_labels if cl}))
|
|
2101
2396
|
|
|
2102
|
-
issue.controlImplementationIds = list(set(finding._control_implementation_ids + cci_control_ids))
|
|
2103
|
-
|
|
2397
|
+
issue.controlImplementationIds = list(set(finding._control_implementation_ids + cci_control_ids))
|
|
2398
|
+
|
|
2399
|
+
def _set_additional_properties(
|
|
2400
|
+
self, issue: regscale_models.Issue, finding: IntegrationFinding, issue_data: Dict[str, Any]
|
|
2401
|
+
) -> None:
|
|
2402
|
+
"""Set additional issue properties."""
|
|
2403
|
+
issue.description = issue_data["description"]
|
|
2404
|
+
issue.sourceReport = finding.source_report or self.title
|
|
2405
|
+
issue.recommendedActions = finding.recommendation_for_mitigation
|
|
2406
|
+
issue.securityChecks = finding.security_check or finding.external_id
|
|
2407
|
+
issue.remediationDescription = issue_data["remediation_description"]
|
|
2408
|
+
issue.integrationFindingId = self.get_finding_identifier(finding)
|
|
2409
|
+
issue.poamComments = finding.poam_comments
|
|
2410
|
+
issue.cve = finding.cve
|
|
2411
|
+
issue.isPoam = issue_data["is_poam"]
|
|
2104
2412
|
issue.basisForAdjustment = (
|
|
2105
2413
|
finding.basis_for_adjustment if finding.basis_for_adjustment else f"{self.title} import"
|
|
2106
2414
|
)
|
|
@@ -2116,9 +2424,10 @@ class WizPolicyComplianceIntegration(ComplianceIntegration):
|
|
|
2116
2424
|
issue.dateLastUpdated = get_current_datetime()
|
|
2117
2425
|
issue.affectedControls = finding.affected_controls
|
|
2118
2426
|
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2427
|
+
def _save_or_create_issue_record(
|
|
2428
|
+
self, issue: regscale_models.Issue, finding: IntegrationFinding, existing_issue, is_poam: bool
|
|
2429
|
+
) -> regscale_models.Issue:
|
|
2430
|
+
"""Save or create the issue record."""
|
|
2122
2431
|
if existing_issue:
|
|
2123
2432
|
logger.debug(f"Saving existing issue {issue.id} with assessmentId={issue.assessmentId}")
|
|
2124
2433
|
issue.save(bulk=True)
|
|
@@ -2131,20 +2440,18 @@ class WizPolicyComplianceIntegration(ComplianceIntegration):
|
|
|
2131
2440
|
logger.debug(f"Issue created with ID: {issue.id}")
|
|
2132
2441
|
self.extra_data_to_properties(finding, issue.id)
|
|
2133
2442
|
else:
|
|
2134
|
-
logger.error(f"
|
|
2443
|
+
logger.error(f"Issue creation failed - no ID returned for finding {finding.external_id}")
|
|
2135
2444
|
return None
|
|
2445
|
+
return issue
|
|
2136
2446
|
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
self._handle_property_and_milestone_creation(issue, finding, existing_issue)
|
|
2144
|
-
else:
|
|
2145
|
-
logger.debug("Skipping milestone creation - issue has no ID")
|
|
2447
|
+
def _handle_post_creation_tasks(
|
|
2448
|
+
self, issue: regscale_models.Issue, finding: IntegrationFinding, existing_issue
|
|
2449
|
+
) -> None:
|
|
2450
|
+
"""Handle tasks after issue creation/update."""
|
|
2451
|
+
if existing_issue and ScannerVariables.useMilestones:
|
|
2452
|
+
self._ensure_issue_has_milestone(issue, finding)
|
|
2146
2453
|
|
|
2147
|
-
|
|
2454
|
+
self._handle_property_and_milestone_creation(issue, finding, existing_issue)
|
|
2148
2455
|
|
|
2149
2456
|
def _populate_compliance_fields_on_finding(self, finding: IntegrationFinding) -> None:
|
|
2150
2457
|
"""
|
|
@@ -2164,7 +2471,6 @@ class WizPolicyComplianceIntegration(ComplianceIntegration):
|
|
|
2164
2471
|
if hasattr(finding, "rule_id") and finding.rule_id:
|
|
2165
2472
|
control_id = self._normalize_control_id_string(finding.rule_id)
|
|
2166
2473
|
if control_id:
|
|
2167
|
-
|
|
2168
2474
|
# Get control implementation ID
|
|
2169
2475
|
impl_id = self._issue_field_setter._get_or_find_implementation_id(control_id)
|
|
2170
2476
|
if impl_id:
|
|
@@ -2628,7 +2934,6 @@ class WizPolicyComplianceIntegration(ComplianceIntegration):
|
|
|
2628
2934
|
"""
|
|
2629
2935
|
logger.info("Starting control assessment processing for Wiz compliance integration")
|
|
2630
2936
|
|
|
2631
|
-
# Ensure existing records cache is loaded
|
|
2632
2937
|
self._load_existing_records_cache()
|
|
2633
2938
|
|
|
2634
2939
|
implementations = self._get_control_implementations()
|
|
@@ -2636,61 +2941,96 @@ class WizPolicyComplianceIntegration(ComplianceIntegration):
|
|
|
2636
2941
|
logger.warning("No control implementations found for assessment processing")
|
|
2637
2942
|
return
|
|
2638
2943
|
|
|
2639
|
-
|
|
2944
|
+
validated_controls = self._validate_controls_with_assets()
|
|
2945
|
+
if not validated_controls["controls_with_assets"]:
|
|
2946
|
+
logger.warning("No controls have assets in RegScale boundary - no control assessments will be created")
|
|
2947
|
+
logger.info("SUMMARY: 0 control assessments created (no assets exist in RegScale)")
|
|
2948
|
+
return
|
|
2949
|
+
|
|
2950
|
+
assessments_created = self._create_assessments_for_validated_controls(
|
|
2951
|
+
validated_controls["controls_with_assets"], implementations
|
|
2952
|
+
)
|
|
2953
|
+
self._log_assessment_summary(assessments_created, validated_controls)
|
|
2954
|
+
|
|
2955
|
+
def _validate_controls_with_assets(self) -> Dict[str, Any]:
|
|
2956
|
+
"""Validate controls and identify those with existing assets."""
|
|
2640
2957
|
all_potential_controls = set(self.passing_controls.keys()) | set(self.failing_controls.keys())
|
|
2641
2958
|
logger.debug(
|
|
2642
2959
|
f"Found {len(all_potential_controls)} potential controls from compliance data: {sorted(all_potential_controls)}"
|
|
2643
2960
|
)
|
|
2644
2961
|
|
|
2645
|
-
# Validate each control has actual assets in our boundary before processing
|
|
2646
2962
|
validated_controls_with_assets = {}
|
|
2647
2963
|
validated_passing_controls = {}
|
|
2648
2964
|
validated_failing_controls = {}
|
|
2649
2965
|
|
|
2650
2966
|
for control_id in all_potential_controls:
|
|
2651
|
-
|
|
2967
|
+
validation_result = self._validate_single_control(control_id)
|
|
2968
|
+
|
|
2969
|
+
if validation_result["should_process"]:
|
|
2970
|
+
validated_controls_with_assets[control_id] = validation_result["asset_identifiers"]
|
|
2971
|
+
|
|
2972
|
+
if control_id in self.failing_controls:
|
|
2973
|
+
validated_failing_controls[control_id] = self.failing_controls[control_id]
|
|
2974
|
+
elif control_id in self.passing_controls:
|
|
2975
|
+
validated_passing_controls[control_id] = self.passing_controls[control_id]
|
|
2976
|
+
|
|
2977
|
+
return {
|
|
2978
|
+
"controls_with_assets": validated_controls_with_assets,
|
|
2979
|
+
"passing_controls": validated_passing_controls,
|
|
2980
|
+
"failing_controls": validated_failing_controls,
|
|
2981
|
+
}
|
|
2982
|
+
|
|
2983
|
+
def _validate_single_control(self, control_id: str) -> Dict[str, Any]:
|
|
2984
|
+
"""Validate a single control for asset existence."""
|
|
2985
|
+
is_passing_control = control_id in self.passing_controls
|
|
2986
|
+
|
|
2987
|
+
if is_passing_control:
|
|
2988
|
+
control_items = self._get_control_compliance_items(control_id)
|
|
2989
|
+
else:
|
|
2652
2990
|
control_items = self._get_validated_control_compliance_items(control_id)
|
|
2653
2991
|
|
|
2654
|
-
|
|
2655
|
-
|
|
2992
|
+
if not control_items and is_passing_control:
|
|
2993
|
+
logger.debug(f"Control {control_id} is passing - will process for compliance documentation")
|
|
2994
|
+
return {"should_process": True, "asset_identifiers": []}
|
|
2656
2995
|
|
|
2657
|
-
|
|
2658
|
-
asset_identifiers
|
|
2659
|
-
assets_found = 0
|
|
2660
|
-
|
|
2661
|
-
for item in control_items:
|
|
2662
|
-
if hasattr(item, "resource_name") and item.resource_name:
|
|
2663
|
-
resource_id = getattr(item, "resource_id", "")
|
|
2664
|
-
# Verify the asset actually exists in RegScale
|
|
2665
|
-
if self._asset_exists_in_regscale(resource_id):
|
|
2666
|
-
asset_identifiers.add(item.resource_name)
|
|
2667
|
-
assets_found += 1
|
|
2668
|
-
else:
|
|
2669
|
-
logger.debug(
|
|
2670
|
-
f"Control {control_id}: Asset {resource_id} ({item.resource_name}) not found in RegScale"
|
|
2671
|
-
)
|
|
2672
|
-
logger.debug(f"Found {assets_found} valid assets for control {control_id}")
|
|
2673
|
-
if not asset_identifiers:
|
|
2674
|
-
continue
|
|
2996
|
+
if not control_items:
|
|
2997
|
+
return {"should_process": False, "asset_identifiers": []}
|
|
2675
2998
|
|
|
2676
|
-
|
|
2677
|
-
validated_controls_with_assets[control_id] = list(asset_identifiers)
|
|
2999
|
+
asset_identifiers = self._collect_asset_identifiers(control_items, control_id, is_passing_control)
|
|
2678
3000
|
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
elif control_id in self.passing_controls:
|
|
2683
|
-
validated_passing_controls[control_id] = self.passing_controls[control_id]
|
|
3001
|
+
# For passing controls, allow through even without assets
|
|
3002
|
+
# For failing controls, require at least one asset
|
|
3003
|
+
should_process = bool(asset_identifiers) or is_passing_control
|
|
2684
3004
|
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
3005
|
+
return {"should_process": should_process, "asset_identifiers": list(asset_identifiers)}
|
|
3006
|
+
|
|
3007
|
+
def _collect_asset_identifiers(self, control_items: List[Any], control_id: str, is_passing_control: bool) -> set:
|
|
3008
|
+
"""Collect asset identifiers for control items."""
|
|
3009
|
+
asset_identifiers = set()
|
|
3010
|
+
assets_found = 0
|
|
3011
|
+
|
|
3012
|
+
for item in control_items:
|
|
3013
|
+
if hasattr(item, "resource_name") and item.resource_name:
|
|
3014
|
+
resource_id = getattr(item, "resource_id", "")
|
|
3015
|
+
# Verify the asset actually exists in RegScale (if not a passing control)
|
|
3016
|
+
if is_passing_control or self._asset_exists_in_regscale(resource_id):
|
|
3017
|
+
asset_identifiers.add(item.resource_name)
|
|
3018
|
+
assets_found += 1
|
|
3019
|
+
else:
|
|
3020
|
+
logger.debug(
|
|
3021
|
+
f"Control {control_id}: Asset {resource_id} ({item.resource_name}) not found in RegScale"
|
|
3022
|
+
)
|
|
2689
3023
|
|
|
3024
|
+
logger.debug(f"Found {assets_found} valid assets for control {control_id}")
|
|
3025
|
+
return asset_identifiers
|
|
3026
|
+
|
|
3027
|
+
def _create_assessments_for_validated_controls(
|
|
3028
|
+
self, validated_controls_with_assets: Dict[str, List[str]], implementations: List[Any]
|
|
3029
|
+
) -> int:
|
|
3030
|
+
"""Create assessments for validated controls."""
|
|
2690
3031
|
assessments_created = 0
|
|
2691
3032
|
processed_impl_today: set[int] = set()
|
|
2692
3033
|
|
|
2693
|
-
# Only process validated controls that have assets in our boundary
|
|
2694
3034
|
for control_id in validated_controls_with_assets.keys():
|
|
2695
3035
|
created = self._process_single_control_assessment(
|
|
2696
3036
|
control_id=control_id,
|
|
@@ -2699,8 +3039,13 @@ class WizPolicyComplianceIntegration(ComplianceIntegration):
|
|
|
2699
3039
|
)
|
|
2700
3040
|
assessments_created += created
|
|
2701
3041
|
|
|
2702
|
-
|
|
2703
|
-
|
|
3042
|
+
return assessments_created
|
|
3043
|
+
|
|
3044
|
+
def _log_assessment_summary(self, assessments_created: int, validated_controls: Dict[str, Any]) -> None:
|
|
3045
|
+
"""Log summary of assessment creation."""
|
|
3046
|
+
validated_control_ids = set(validated_controls["controls_with_assets"].keys())
|
|
3047
|
+
validated_failing_controls = validated_controls["failing_controls"]
|
|
3048
|
+
|
|
2704
3049
|
passing_assessments = len([cid for cid in validated_control_ids if cid not in validated_failing_controls])
|
|
2705
3050
|
failing_assessments = len([cid for cid in validated_control_ids if cid in validated_failing_controls])
|
|
2706
3051
|
|
|
@@ -2710,11 +3055,11 @@ class WizPolicyComplianceIntegration(ComplianceIntegration):
|
|
|
2710
3055
|
)
|
|
2711
3056
|
else:
|
|
2712
3057
|
logger.warning(
|
|
2713
|
-
f"No control assessments were actually created (0 assessments) despite finding {len(
|
|
3058
|
+
f"No control assessments were actually created (0 assessments) despite finding {len(validated_controls['controls_with_assets'])} controls with assets"
|
|
2714
3059
|
)
|
|
2715
3060
|
|
|
2716
3061
|
logger.info(
|
|
2717
|
-
f"CONTROL ASSESSMENT SUMMARY: {assessments_created} assessments created for {len(
|
|
3062
|
+
f"CONTROL ASSESSMENT SUMMARY: {assessments_created} assessments created for {len(validated_controls['controls_with_assets'])} validated controls"
|
|
2718
3063
|
)
|
|
2719
3064
|
|
|
2720
3065
|
def _sync_assessment_cache_from_base_class(self) -> None:
|
|
@@ -2989,6 +3334,29 @@ class WizPolicyComplianceIntegration(ComplianceIntegration):
|
|
|
2989
3334
|
# Fallback (should not normally happen since resource_id is required)
|
|
2990
3335
|
return resource_name or "Unknown Resource"
|
|
2991
3336
|
|
|
3337
|
+
def _get_provider_unique_id_for_asset_identifier(self, compliance_item: "WizComplianceItem") -> str:
|
|
3338
|
+
"""
|
|
3339
|
+
Get the provider unique ID for meaningful asset identification in eMASS exports.
|
|
3340
|
+
|
|
3341
|
+
This provides cloud provider-specific identifiers like ARNs, Azure resource IDs, etc.
|
|
3342
|
+
instead of internal Wiz IDs for better readability in POAMs and eMASS exports.
|
|
3343
|
+
|
|
3344
|
+
:param WizComplianceItem compliance_item: Compliance item with resource information
|
|
3345
|
+
:return: Provider unique ID or fallback to resource name/ID
|
|
3346
|
+
:rtype: str
|
|
3347
|
+
"""
|
|
3348
|
+
provider_unique_id = getattr(compliance_item, "provider_unique_id", "")
|
|
3349
|
+
resource_name = getattr(compliance_item, "resource_name", "")
|
|
3350
|
+
resource_id = getattr(compliance_item, "resource_id", "")
|
|
3351
|
+
|
|
3352
|
+
# Priority: providerUniqueId -> resource_name -> resource_id
|
|
3353
|
+
if provider_unique_id:
|
|
3354
|
+
return provider_unique_id
|
|
3355
|
+
elif resource_name:
|
|
3356
|
+
return resource_name
|
|
3357
|
+
else:
|
|
3358
|
+
return resource_id
|
|
3359
|
+
|
|
2992
3360
|
def _create_consolidated_asset_identifier(self, asset_mappings: Dict[str, Dict[str, str]]) -> str:
|
|
2993
3361
|
"""
|
|
2994
3362
|
Create a consolidated asset identifier with only asset names (one per line).
|
|
@@ -3029,6 +3397,56 @@ class WizPolicyComplianceIntegration(ComplianceIntegration):
|
|
|
3029
3397
|
)
|
|
3030
3398
|
return consolidated_identifier
|
|
3031
3399
|
|
|
3400
|
+
def _categorize_controls_by_aggregation(self) -> None:
|
|
3401
|
+
"""
|
|
3402
|
+
Override the base method to handle multiple control IDs per compliance item.
|
|
3403
|
+
Wiz policies can map to multiple NIST controls (e.g., AC-2(4), AC-6(9)) in securitySubCategories.
|
|
3404
|
+
This method ensures all controls from a policy assessment are properly categorized.
|
|
3405
|
+
"""
|
|
3406
|
+
from collections import defaultdict, Counter
|
|
3407
|
+
|
|
3408
|
+
# Group all compliance items by control ID - handle multiple controls per item
|
|
3409
|
+
control_items = defaultdict(list)
|
|
3410
|
+
|
|
3411
|
+
for item in self.all_compliance_items:
|
|
3412
|
+
# Get all control IDs that this compliance item maps to
|
|
3413
|
+
all_control_ids = self._get_all_control_ids_for_compliance_item(item)
|
|
3414
|
+
|
|
3415
|
+
# Add this item to each control it maps to
|
|
3416
|
+
for control_id in all_control_ids:
|
|
3417
|
+
control_key = control_id.lower()
|
|
3418
|
+
control_items[control_key].append(item)
|
|
3419
|
+
|
|
3420
|
+
# Analyze each control's results
|
|
3421
|
+
for control_key, items in control_items.items():
|
|
3422
|
+
results = [item.compliance_result for item in items]
|
|
3423
|
+
result_counts = Counter(results)
|
|
3424
|
+
|
|
3425
|
+
fail_count = sum(result_counts.get(status, 0) for status in self.FAIL_STATUSES)
|
|
3426
|
+
pass_count = sum(result_counts.get(status, 0) for status in self.PASS_STATUSES)
|
|
3427
|
+
|
|
3428
|
+
# Determine control status - strict compliance: ALL assessments must pass
|
|
3429
|
+
if fail_count == 0 and pass_count > 0:
|
|
3430
|
+
# All results are passing - control passes
|
|
3431
|
+
self.passing_controls[control_key] = items[0] # Use first item as representative
|
|
3432
|
+
logger.debug(f"Control {control_key} marked as PASSING: {pass_count}P/{fail_count}F")
|
|
3433
|
+
|
|
3434
|
+
elif fail_count > 0:
|
|
3435
|
+
# Any failures present - control fails (strict compliance)
|
|
3436
|
+
self.failing_controls[control_key] = next(
|
|
3437
|
+
item for item in items if item.compliance_result in self.FAIL_STATUSES
|
|
3438
|
+
)
|
|
3439
|
+
logger.debug(
|
|
3440
|
+
f"Control {control_key} marked as FAILING: {pass_count}P/{fail_count}F (any failure = control fails)"
|
|
3441
|
+
)
|
|
3442
|
+
else:
|
|
3443
|
+
# No pass or fail results - skip this control
|
|
3444
|
+
logger.debug(f"Control {control_key} skipped: no valid pass/fail results")
|
|
3445
|
+
|
|
3446
|
+
logger.info(
|
|
3447
|
+
f"Control categorization complete: {len(self.passing_controls)} passing, {len(self.failing_controls)} failing"
|
|
3448
|
+
)
|
|
3449
|
+
|
|
3032
3450
|
|
|
3033
3451
|
def resolve_framework_id(framework_input: str) -> str:
|
|
3034
3452
|
"""
|