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
|
@@ -25,6 +25,8 @@ from regscale.models.regscale_models import (
|
|
|
25
25
|
ControlImplementation,
|
|
26
26
|
Assessment,
|
|
27
27
|
ImplementationObjective,
|
|
28
|
+
SecurityPlan,
|
|
29
|
+
ComplianceSettings,
|
|
28
30
|
)
|
|
29
31
|
|
|
30
32
|
logger = logging.getLogger("regscale")
|
|
@@ -160,6 +162,10 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
160
162
|
# Set scan date
|
|
161
163
|
self.scan_date = get_current_datetime()
|
|
162
164
|
|
|
165
|
+
# Cache for compliance settings
|
|
166
|
+
self._compliance_settings = None
|
|
167
|
+
self._security_plan = None
|
|
168
|
+
|
|
163
169
|
def is_poam(self, finding: IntegrationFinding) -> bool: # type: ignore[override]
|
|
164
170
|
"""
|
|
165
171
|
Determines if an issue should be considered a POAM for compliance integrations.
|
|
@@ -225,9 +231,7 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
225
231
|
)
|
|
226
232
|
|
|
227
233
|
for asset in existing_assets:
|
|
228
|
-
# Cache by
|
|
229
|
-
if hasattr(asset, "externalId") and asset.externalId:
|
|
230
|
-
self._existing_assets_cache[asset.externalId] = asset
|
|
234
|
+
# Cache by identifier and other_tracking_number for flexible lookup
|
|
231
235
|
if hasattr(asset, "identifier") and asset.identifier:
|
|
232
236
|
self._existing_assets_cache[asset.identifier] = asset
|
|
233
237
|
if hasattr(asset, "otherTrackingNumber") and asset.otherTrackingNumber:
|
|
@@ -283,13 +287,11 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
283
287
|
wiz_issues = 0
|
|
284
288
|
for issue in all_issues:
|
|
285
289
|
# Cache by external_id and other_identifier for flexible lookup
|
|
286
|
-
if hasattr(issue, "externalId") and issue.externalId:
|
|
287
|
-
self._existing_issues_cache[issue.externalId] = issue
|
|
288
|
-
if "wiz-policy" in issue.externalId.lower():
|
|
289
|
-
wiz_issues += 1
|
|
290
|
-
logger.debug(f"Cached Wiz issue: {issue.id} -> external_id: {issue.externalId}")
|
|
291
290
|
if hasattr(issue, "otherIdentifier") and issue.otherIdentifier:
|
|
292
291
|
self._existing_issues_cache[issue.otherIdentifier] = issue
|
|
292
|
+
if "wiz-policy" in issue.otherIdentifier.lower():
|
|
293
|
+
wiz_issues += 1
|
|
294
|
+
logger.debug(f"Cached Wiz issue: {issue.id} -> other_identifier: {issue.otherIdentifier}")
|
|
293
295
|
|
|
294
296
|
logger.debug(f"Cached {wiz_issues} Wiz policy issues out of {len(all_issues)} total issues")
|
|
295
297
|
|
|
@@ -411,17 +413,32 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
411
413
|
"""
|
|
412
414
|
logger.info("Processing compliance data...")
|
|
413
415
|
|
|
414
|
-
|
|
416
|
+
self._reset_compliance_state()
|
|
417
|
+
allowed_controls = self._build_allowed_controls_set()
|
|
418
|
+
raw_compliance_data = self.fetch_compliance_data()
|
|
419
|
+
|
|
420
|
+
processing_stats = self._process_raw_compliance_items(raw_compliance_data, allowed_controls)
|
|
421
|
+
self._log_processing_summary(raw_compliance_data, processing_stats)
|
|
422
|
+
|
|
423
|
+
# Perform control-level categorization based on aggregated results
|
|
424
|
+
self._categorize_controls_by_aggregation()
|
|
425
|
+
self._log_final_results()
|
|
426
|
+
|
|
427
|
+
def _reset_compliance_state(self) -> None:
|
|
428
|
+
"""Reset state to avoid double counting on repeated calls."""
|
|
415
429
|
self.all_compliance_items = []
|
|
416
430
|
self.failed_compliance_items = []
|
|
417
431
|
self.passing_controls = {}
|
|
418
432
|
self.failing_controls = {}
|
|
419
433
|
self.asset_compliance_map.clear()
|
|
420
434
|
|
|
421
|
-
|
|
435
|
+
def _build_allowed_controls_set(self) -> set[str]:
|
|
436
|
+
"""Build allowed control IDs from plan/catalog controls to restrict scope."""
|
|
422
437
|
allowed_controls_normalized: set[str] = set()
|
|
423
438
|
try:
|
|
424
439
|
controls = self._get_controls()
|
|
440
|
+
logger.debug(f"Loaded {len(controls)} controls from plan/catalog")
|
|
441
|
+
|
|
425
442
|
for ctl in controls:
|
|
426
443
|
cid = (ctl.get("controlId") or "").strip()
|
|
427
444
|
if not cid:
|
|
@@ -429,56 +446,220 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
429
446
|
base, sub = self._normalize_control_id(cid)
|
|
430
447
|
normalized = f"{base}({sub})" if sub else base
|
|
431
448
|
allowed_controls_normalized.add(normalized)
|
|
432
|
-
|
|
433
|
-
|
|
449
|
+
|
|
450
|
+
logger.debug(f"Built allowed_controls_normalized set with {len(allowed_controls_normalized)} entries")
|
|
451
|
+
if allowed_controls_normalized:
|
|
452
|
+
sample = sorted(allowed_controls_normalized)[:5]
|
|
453
|
+
logger.debug(f"Sample allowed controls: {sample}")
|
|
454
|
+
except Exception as e:
|
|
455
|
+
logger.warning(f"Could not load controls from plan/catalog: {e}")
|
|
434
456
|
allowed_controls_normalized = set()
|
|
435
457
|
|
|
436
|
-
|
|
437
|
-
|
|
458
|
+
return allowed_controls_normalized
|
|
459
|
+
|
|
460
|
+
def _process_raw_compliance_items(self, raw_compliance_data: list, allowed_controls: set) -> dict:
|
|
461
|
+
"""Process raw compliance items and return processing statistics.
|
|
462
|
+
:param list raw_compliance_data: Raw compliance data from external system
|
|
463
|
+
:param set allowed_controls: Allowed control IDs
|
|
464
|
+
:return: Processed compliance items
|
|
465
|
+
:rtype: dict
|
|
466
|
+
"""
|
|
467
|
+
stats = {"skipped_no_control": 0, "skipped_no_resource": 0, "skipped_not_in_plan": 0, "processed_count": 0}
|
|
438
468
|
|
|
439
|
-
# Convert to ComplianceItem objects
|
|
440
469
|
for raw_item in raw_compliance_data:
|
|
441
470
|
try:
|
|
442
471
|
compliance_item = self.create_compliance_item(raw_item)
|
|
443
|
-
|
|
444
|
-
if not getattr(compliance_item, "control_id", "") or not getattr(compliance_item, "resource_id", ""):
|
|
472
|
+
if not self._process_single_compliance_item(compliance_item, allowed_controls, stats):
|
|
445
473
|
continue
|
|
446
|
-
|
|
447
|
-
# If we have an allowed set, restrict to only controls in current plan/catalog
|
|
448
|
-
if allowed_controls_normalized:
|
|
449
|
-
base, sub = self._normalize_control_id(getattr(compliance_item, "control_id", ""))
|
|
450
|
-
norm_item = f"{base}({sub})" if sub else base
|
|
451
|
-
if norm_item not in allowed_controls_normalized:
|
|
452
|
-
continue
|
|
453
|
-
self.all_compliance_items.append(compliance_item)
|
|
454
|
-
|
|
455
|
-
# Build asset mapping
|
|
456
|
-
self.asset_compliance_map[compliance_item.resource_id].append(compliance_item)
|
|
457
|
-
|
|
458
|
-
# Categorize by result
|
|
459
|
-
if compliance_item.compliance_result in self.FAIL_STATUSES:
|
|
460
|
-
self.failed_compliance_items.append(compliance_item)
|
|
461
|
-
# Track failing controls (control can fail if ANY asset fails)
|
|
462
|
-
control_key = compliance_item.control_id.lower()
|
|
463
|
-
self.failing_controls[control_key] = compliance_item
|
|
464
|
-
# Remove from passing if it was there
|
|
465
|
-
self.passing_controls.pop(control_key, None)
|
|
466
|
-
|
|
467
|
-
elif compliance_item.compliance_result in self.PASS_STATUSES:
|
|
468
|
-
control_key = compliance_item.control_id.lower()
|
|
469
|
-
# Only mark as passing if not already failing
|
|
470
|
-
if control_key not in self.failing_controls:
|
|
471
|
-
self.passing_controls[control_key] = compliance_item
|
|
472
|
-
|
|
473
474
|
except Exception as e:
|
|
474
475
|
logger.error(f"Error processing compliance item: {e}")
|
|
475
476
|
continue
|
|
476
477
|
|
|
478
|
+
return stats
|
|
479
|
+
|
|
480
|
+
def _process_single_compliance_item(self, compliance_item: Any, allowed_controls: set, stats: dict) -> bool:
|
|
481
|
+
"""Process a single compliance item and update statistics. Returns True if processed successfully."""
|
|
482
|
+
control_id = getattr(compliance_item, "control_id", "")
|
|
483
|
+
resource_id = getattr(compliance_item, "resource_id", "")
|
|
484
|
+
|
|
485
|
+
if not control_id:
|
|
486
|
+
stats["skipped_no_control"] += 1
|
|
487
|
+
return False
|
|
488
|
+
if not resource_id:
|
|
489
|
+
stats["skipped_no_resource"] += 1
|
|
490
|
+
return False
|
|
491
|
+
|
|
492
|
+
if not self._should_process_item(compliance_item, control_id, allowed_controls, stats):
|
|
493
|
+
return False
|
|
494
|
+
|
|
495
|
+
self._add_processed_item(compliance_item, stats)
|
|
496
|
+
return True
|
|
497
|
+
|
|
498
|
+
def _should_process_item(self, compliance_item: Any, control_id: str, allowed_controls: set, stats: dict) -> bool:
|
|
499
|
+
"""Determine if an item should be processed based on control filtering."""
|
|
500
|
+
if not allowed_controls:
|
|
501
|
+
return True
|
|
502
|
+
|
|
503
|
+
base, sub = self._normalize_control_id(control_id)
|
|
504
|
+
norm_item = f"{base}({sub})" if sub else base
|
|
505
|
+
|
|
506
|
+
if norm_item in allowed_controls:
|
|
507
|
+
return True
|
|
508
|
+
|
|
509
|
+
# Allow PASS controls through even if they don't have existing implementations
|
|
510
|
+
if compliance_item.compliance_result in self.PASS_STATUSES:
|
|
511
|
+
return True
|
|
512
|
+
|
|
513
|
+
stats["skipped_not_in_plan"] += 1
|
|
514
|
+
if stats["skipped_not_in_plan"] <= 3:
|
|
515
|
+
logger.debug(f"Skipping control {norm_item} - not in plan (result: {compliance_item.compliance_result})")
|
|
516
|
+
return False
|
|
517
|
+
|
|
518
|
+
def _add_processed_item(self, compliance_item: Any, stats: dict) -> None:
|
|
519
|
+
"""Add a processed item to collections and update statistics."""
|
|
520
|
+
self.all_compliance_items.append(compliance_item)
|
|
521
|
+
stats["processed_count"] += 1
|
|
522
|
+
|
|
523
|
+
# Build asset mapping
|
|
524
|
+
self.asset_compliance_map[compliance_item.resource_id].append(compliance_item)
|
|
525
|
+
|
|
526
|
+
# Categorize by result
|
|
527
|
+
if compliance_item.compliance_result in self.FAIL_STATUSES:
|
|
528
|
+
self.failed_compliance_items.append(compliance_item)
|
|
529
|
+
|
|
530
|
+
def _log_processing_summary(self, raw_compliance_data: list, stats: dict) -> None:
|
|
531
|
+
"""Log summary of compliance data processing."""
|
|
532
|
+
logger.debug("Compliance item processing summary:")
|
|
533
|
+
logger.debug(f" - Total raw items: {len(raw_compliance_data)}")
|
|
534
|
+
logger.debug(f" - Skipped (no control_id): {stats['skipped_no_control']}")
|
|
535
|
+
logger.debug(f" - Skipped (no resource_id): {stats['skipped_no_resource']}")
|
|
536
|
+
logger.debug(f" - Skipped (not in plan): {stats['skipped_not_in_plan']}")
|
|
537
|
+
logger.debug(f" - Processed successfully: {stats['processed_count']}")
|
|
538
|
+
|
|
539
|
+
def _log_final_results(self) -> None:
|
|
540
|
+
"""Log final processing results."""
|
|
477
541
|
logger.debug(
|
|
478
542
|
f"Processed {len(self.all_compliance_items)} compliance items: "
|
|
479
543
|
f"{len(self.all_compliance_items) - len(self.failed_compliance_items)} passing, "
|
|
480
544
|
f"{len(self.failed_compliance_items)} failing"
|
|
481
545
|
)
|
|
546
|
+
logger.debug(
|
|
547
|
+
f"Control categorization: {len(self.passing_controls)} passing controls, "
|
|
548
|
+
f"{len(self.failing_controls)} failing controls"
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
def _categorize_controls_by_aggregation(self) -> None:
|
|
552
|
+
"""
|
|
553
|
+
Categorize controls as passing or failing based on aggregated results across all compliance items.
|
|
554
|
+
|
|
555
|
+
This method uses project-scoped aggregation logic instead of the previous "any fail = control fails"
|
|
556
|
+
approach. For project-scoped integrations (like Wiz), this provides more accurate control status.
|
|
557
|
+
"""
|
|
558
|
+
|
|
559
|
+
# Group all compliance items by control ID
|
|
560
|
+
control_items = self._group_items_by_control()
|
|
561
|
+
|
|
562
|
+
# Analyze each control's results
|
|
563
|
+
for control_key, items in control_items.items():
|
|
564
|
+
self._categorize_single_control(control_key, items)
|
|
565
|
+
|
|
566
|
+
def _group_items_by_control(self) -> dict:
|
|
567
|
+
"""Group compliance items by control ID."""
|
|
568
|
+
from collections import defaultdict
|
|
569
|
+
|
|
570
|
+
control_items = defaultdict(list)
|
|
571
|
+
for item in self.all_compliance_items:
|
|
572
|
+
control_key = item.control_id.lower()
|
|
573
|
+
control_items[control_key].append(item)
|
|
574
|
+
|
|
575
|
+
return control_items
|
|
576
|
+
|
|
577
|
+
def _categorize_single_control(self, control_key: str, items: list) -> None:
|
|
578
|
+
"""Categorize a single control based on its compliance items."""
|
|
579
|
+
from collections import Counter
|
|
580
|
+
|
|
581
|
+
results = [item.compliance_result for item in items]
|
|
582
|
+
result_counts = Counter(results)
|
|
583
|
+
total_items = len(results)
|
|
584
|
+
|
|
585
|
+
fail_count, pass_count = self._count_pass_fail_results(result_counts)
|
|
586
|
+
|
|
587
|
+
if fail_count == 0 and pass_count > 0:
|
|
588
|
+
self._mark_control_as_passing(control_key, items, pass_count, fail_count)
|
|
589
|
+
elif fail_count > 0:
|
|
590
|
+
self._handle_control_with_failures(control_key, items, fail_count, pass_count, total_items)
|
|
591
|
+
else:
|
|
592
|
+
logger.debug(f"Control {control_key} has unclear results: {dict(result_counts)}")
|
|
593
|
+
|
|
594
|
+
def _count_pass_fail_results(self, result_counts: dict) -> tuple[int, int]:
|
|
595
|
+
"""Count pass and fail results from result counts."""
|
|
596
|
+
fail_statuses_lower = [status.lower() for status in self.FAIL_STATUSES]
|
|
597
|
+
pass_statuses_lower = [status.lower() for status in self.PASS_STATUSES]
|
|
598
|
+
|
|
599
|
+
fail_count = 0
|
|
600
|
+
pass_count = 0
|
|
601
|
+
|
|
602
|
+
for result, count in result_counts.items():
|
|
603
|
+
result_lower = result.lower()
|
|
604
|
+
if result_lower in fail_statuses_lower:
|
|
605
|
+
fail_count += count
|
|
606
|
+
elif result_lower in pass_statuses_lower:
|
|
607
|
+
pass_count += count
|
|
608
|
+
|
|
609
|
+
return fail_count, pass_count
|
|
610
|
+
|
|
611
|
+
def _mark_control_as_passing(self, control_key: str, items: list, pass_count: int, fail_count: int) -> None:
|
|
612
|
+
"""Mark a control as passing."""
|
|
613
|
+
self.passing_controls[control_key] = items[0] # Use first item as representative
|
|
614
|
+
logger.debug(f"Control {control_key} marked as PASSING: {pass_count}P/{fail_count}F")
|
|
615
|
+
|
|
616
|
+
def _handle_control_with_failures(
|
|
617
|
+
self, control_key: str, items: list, fail_count: int, pass_count: int, total_items: int
|
|
618
|
+
) -> None:
|
|
619
|
+
"""Handle a control that has some failures."""
|
|
620
|
+
fail_ratio = fail_count / total_items
|
|
621
|
+
failure_threshold = getattr(self, "control_failure_threshold", 0.2)
|
|
622
|
+
|
|
623
|
+
if fail_ratio > failure_threshold:
|
|
624
|
+
self._mark_control_as_failing(control_key, items, pass_count, fail_count, fail_ratio, failure_threshold)
|
|
625
|
+
else:
|
|
626
|
+
self._mark_control_as_passing_with_warnings(
|
|
627
|
+
control_key, items, pass_count, fail_count, fail_ratio, failure_threshold
|
|
628
|
+
)
|
|
629
|
+
|
|
630
|
+
def _mark_control_as_failing(
|
|
631
|
+
self,
|
|
632
|
+
control_key: str,
|
|
633
|
+
items: list,
|
|
634
|
+
pass_count: int,
|
|
635
|
+
fail_count: int,
|
|
636
|
+
fail_ratio: float,
|
|
637
|
+
failure_threshold: float,
|
|
638
|
+
) -> None:
|
|
639
|
+
"""Mark a control as failing due to significant failures."""
|
|
640
|
+
fail_statuses_lower = [status.lower() for status in self.FAIL_STATUSES]
|
|
641
|
+
failing_item = next(item for item in items if item.compliance_result.lower() in fail_statuses_lower)
|
|
642
|
+
self.failing_controls[control_key] = failing_item
|
|
643
|
+
logger.debug(
|
|
644
|
+
f"Control {control_key} marked as FAILING: {pass_count}P/{fail_count}F "
|
|
645
|
+
f"({fail_ratio:.1%} fail rate > {failure_threshold:.1%} threshold)"
|
|
646
|
+
)
|
|
647
|
+
|
|
648
|
+
def _mark_control_as_passing_with_warnings(
|
|
649
|
+
self,
|
|
650
|
+
control_key: str,
|
|
651
|
+
items: list,
|
|
652
|
+
pass_count: int,
|
|
653
|
+
fail_count: int,
|
|
654
|
+
fail_ratio: float,
|
|
655
|
+
failure_threshold: float,
|
|
656
|
+
) -> None:
|
|
657
|
+
"""Mark a control as passing despite low failure rate."""
|
|
658
|
+
self.passing_controls[control_key] = items[0]
|
|
659
|
+
logger.debug(
|
|
660
|
+
f"Control {control_key} marked as PASSING (low fail rate): {pass_count}P/{fail_count}F "
|
|
661
|
+
f"({fail_ratio:.1%} fail rate < {failure_threshold:.1%} threshold)"
|
|
662
|
+
)
|
|
482
663
|
|
|
483
664
|
def create_asset_from_compliance_item(self, compliance_item: ComplianceItem) -> Optional[IntegrationAsset]:
|
|
484
665
|
"""
|
|
@@ -660,6 +841,11 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
660
841
|
assets_processed = self.update_regscale_assets(iter(assets))
|
|
661
842
|
self._log_asset_results(assets_processed)
|
|
662
843
|
|
|
844
|
+
# Refresh the asset map after creating/updating assets to ensure
|
|
845
|
+
# the map contains all assets for issue creation
|
|
846
|
+
logger.debug("Refreshing asset map after asset sync...")
|
|
847
|
+
self.asset_map_by_identifier.update(self.get_asset_map())
|
|
848
|
+
|
|
663
849
|
def _log_asset_results(self, assets_processed: int) -> None:
|
|
664
850
|
"""
|
|
665
851
|
Log asset processing results.
|
|
@@ -707,8 +893,32 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
707
893
|
logger.debug("No findings to process into issues")
|
|
708
894
|
return
|
|
709
895
|
|
|
710
|
-
|
|
711
|
-
|
|
896
|
+
# Ensure asset map is populated before processing issues
|
|
897
|
+
# This handles cases where assets were created in previous runs
|
|
898
|
+
if not self.asset_map_by_identifier:
|
|
899
|
+
logger.debug("Loading asset map before issue processing...")
|
|
900
|
+
self.asset_map_by_identifier.update(self.get_asset_map())
|
|
901
|
+
|
|
902
|
+
findings_processed, findings_skipped = self._process_findings_to_issues(findings)
|
|
903
|
+
|
|
904
|
+
# CRITICAL FIX: Flush bulk issue operations to database
|
|
905
|
+
# This ensures all issues created/updated in bulk mode are persisted
|
|
906
|
+
logger.debug(f"Calling bulk_save for {findings_processed} processed findings ({findings_skipped} skipped)...")
|
|
907
|
+
issue_results = regscale_models.Issue.bulk_save()
|
|
908
|
+
logger.debug(
|
|
909
|
+
f"Bulk save completed - created: {issue_results.get('created_count', 0)}, updated: {issue_results.get('updated_count', 0)}"
|
|
910
|
+
)
|
|
911
|
+
|
|
912
|
+
# Update result counts with actual database operations
|
|
913
|
+
if hasattr(self, "_results"):
|
|
914
|
+
if "issues" not in self._results:
|
|
915
|
+
self._results["issues"] = {}
|
|
916
|
+
self._results["issues"].update(issue_results)
|
|
917
|
+
|
|
918
|
+
# Use actual database results for logging
|
|
919
|
+
issues_created = issue_results.get("created_count", 0)
|
|
920
|
+
issues_updated = issue_results.get("updated_count", 0)
|
|
921
|
+
self._log_issue_results_accurate(issues_created, issues_updated, findings_processed, findings_skipped)
|
|
712
922
|
|
|
713
923
|
def _process_findings_to_issues(self, findings: List[IntegrationFinding]) -> tuple[int, int]:
|
|
714
924
|
"""
|
|
@@ -720,14 +930,20 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
720
930
|
issues_created = 0
|
|
721
931
|
issues_skipped = 0
|
|
722
932
|
|
|
723
|
-
|
|
933
|
+
logger.debug(f"Processing {len(findings)} findings into issues...")
|
|
934
|
+
for i, finding in enumerate(findings):
|
|
724
935
|
try:
|
|
936
|
+
logger.debug(
|
|
937
|
+
f"Processing finding {i + 1}/{len(findings)}: external_id='{finding.external_id}', asset_identifier='{finding.asset_identifier}"
|
|
938
|
+
)
|
|
725
939
|
if self._process_single_finding(finding):
|
|
726
940
|
issues_created += 1
|
|
941
|
+
logger.debug(f" -> Finding {i + 1} processed successfully")
|
|
727
942
|
else:
|
|
728
943
|
issues_skipped += 1
|
|
944
|
+
logger.debug(f" -> Finding {i + 1} skipped")
|
|
729
945
|
except Exception as e:
|
|
730
|
-
logger.error(f"Error processing finding: {e}")
|
|
946
|
+
logger.error(f"Error processing finding {i + 1}: {e}")
|
|
731
947
|
issues_skipped += 1
|
|
732
948
|
|
|
733
949
|
return issues_created, issues_skipped
|
|
@@ -739,14 +955,25 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
739
955
|
:param finding: Finding to process
|
|
740
956
|
:return: True if issue was created/updated, False if skipped
|
|
741
957
|
"""
|
|
958
|
+
logger.debug(
|
|
959
|
+
f" -> Processing finding: external_id='{finding.external_id}', asset_identifier='{finding.asset_identifier}'"
|
|
960
|
+
)
|
|
961
|
+
|
|
742
962
|
asset = self._get_or_create_asset_for_finding(finding)
|
|
743
963
|
if not asset:
|
|
964
|
+
logger.debug(f" -> Asset not found/created for identifier '{finding.asset_identifier}', skipping finding")
|
|
744
965
|
self._log_asset_not_found_error(finding)
|
|
745
966
|
return False
|
|
746
967
|
|
|
968
|
+
logger.debug(f" -> Found/created asset {asset.id} for identifier '{finding.asset_identifier}'")
|
|
747
969
|
issue_title = self.get_issue_title(finding)
|
|
748
970
|
issue = self.create_or_update_issue_from_finding(title=issue_title, finding=finding)
|
|
749
|
-
|
|
971
|
+
success = issue is not None
|
|
972
|
+
if success and issue:
|
|
973
|
+
logger.debug(f" -> Successfully processed finding -> issue {issue.id}")
|
|
974
|
+
else:
|
|
975
|
+
logger.debug(" -> Failed to create/update issue for finding")
|
|
976
|
+
return success
|
|
750
977
|
|
|
751
978
|
def _get_or_create_asset_for_finding(self, finding: IntegrationFinding) -> Optional[regscale_models.Asset]:
|
|
752
979
|
"""
|
|
@@ -778,6 +1005,7 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
778
1005
|
def _log_issue_results(self, issues_created: int, issues_skipped: int) -> None:
|
|
779
1006
|
"""
|
|
780
1007
|
Log issue processing results.
|
|
1008
|
+
DEPRECATED: Use _log_issue_results_accurate for accurate reporting.
|
|
781
1009
|
|
|
782
1010
|
:param int issues_created: Number of issues created/updated
|
|
783
1011
|
:param int issues_skipped: Number of issues skipped
|
|
@@ -791,6 +1019,36 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
791
1019
|
else:
|
|
792
1020
|
logger.debug("No issues processed")
|
|
793
1021
|
|
|
1022
|
+
def _log_issue_results_accurate(
|
|
1023
|
+
self, issues_created: int, issues_updated: int, findings_processed: int, findings_skipped: int
|
|
1024
|
+
) -> None:
|
|
1025
|
+
"""
|
|
1026
|
+
Log accurate issue processing results based on actual database operations.
|
|
1027
|
+
|
|
1028
|
+
:param int issues_created: Number of new issues created in database
|
|
1029
|
+
:param int issues_updated: Number of existing issues updated in database
|
|
1030
|
+
:param int findings_processed: Number of findings that were processed
|
|
1031
|
+
:param int findings_skipped: Number of findings that were skipped
|
|
1032
|
+
:return: None
|
|
1033
|
+
:rtype: None
|
|
1034
|
+
"""
|
|
1035
|
+
total_db_operations = issues_created + issues_updated
|
|
1036
|
+
|
|
1037
|
+
if total_db_operations > 0:
|
|
1038
|
+
logger.info(
|
|
1039
|
+
f"Processed {findings_processed} findings into issues: {issues_created} new issues created, {issues_updated} existing issues updated"
|
|
1040
|
+
)
|
|
1041
|
+
if findings_skipped > 0:
|
|
1042
|
+
logger.info(f"Skipped {findings_skipped} findings (assets not found)")
|
|
1043
|
+
elif findings_skipped > 0:
|
|
1044
|
+
logger.warning(
|
|
1045
|
+
f"Issues processed: 0 created/updated, {findings_skipped} findings skipped (assets not found)"
|
|
1046
|
+
)
|
|
1047
|
+
else:
|
|
1048
|
+
logger.debug(
|
|
1049
|
+
f"Processed {findings_processed} findings but no database changes were needed (all issues up-to-date)"
|
|
1050
|
+
)
|
|
1051
|
+
|
|
794
1052
|
def _finalize_scan_history(self, scan_history: regscale_models.ScanHistory) -> None:
|
|
795
1053
|
"""
|
|
796
1054
|
Finalize scan history with error handling.
|
|
@@ -1135,6 +1393,7 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
1135
1393
|
def _parse_control_id(control_id: str) -> tuple[str, Optional[str]]:
|
|
1136
1394
|
"""
|
|
1137
1395
|
Parse a control id like 'AC-2(1)', 'AC-2 (1)', 'AC-2-1' into (base, sub).
|
|
1396
|
+
Normalizes leading zeros (e.g., AC-01 becomes AC-1).
|
|
1138
1397
|
|
|
1139
1398
|
Returns (base, None) when no subcontrol.
|
|
1140
1399
|
|
|
@@ -1148,8 +1407,22 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
1148
1407
|
if not m:
|
|
1149
1408
|
return cid.upper(), None
|
|
1150
1409
|
base = m.group(1).upper()
|
|
1410
|
+
# Normalize leading zeros in base control number (e.g., AC-01 -> AC-1)
|
|
1411
|
+
if "-" in base:
|
|
1412
|
+
prefix, number = base.split("-", 1)
|
|
1413
|
+
try:
|
|
1414
|
+
normalized_number = str(int(number))
|
|
1415
|
+
base = f"{prefix}-{normalized_number}"
|
|
1416
|
+
except ValueError:
|
|
1417
|
+
pass # Keep original if conversion fails
|
|
1151
1418
|
# Subcontrol may be captured in group 2, 3, or 4 depending on the branch matched
|
|
1152
1419
|
sub = m.group(2) or m.group(3) or m.group(4)
|
|
1420
|
+
# Normalize leading zeros in subcontrol (e.g., 01 -> 1)
|
|
1421
|
+
if sub:
|
|
1422
|
+
try:
|
|
1423
|
+
sub = str(int(sub))
|
|
1424
|
+
except ValueError:
|
|
1425
|
+
pass # Keep original if conversion fails
|
|
1153
1426
|
return base, sub
|
|
1154
1427
|
|
|
1155
1428
|
@classmethod
|
|
@@ -1181,6 +1454,7 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
1181
1454
|
def _normalize_control_id(control_id: str) -> tuple[str, Optional[str]]:
|
|
1182
1455
|
"""
|
|
1183
1456
|
Normalize control id to a canonical tuple (BASE, SUB) for set membership.
|
|
1457
|
+
Normalizes leading zeros (e.g., AC-01 becomes AC-1).
|
|
1184
1458
|
|
|
1185
1459
|
:param str control_id: Control identifier to normalize
|
|
1186
1460
|
:return: Tuple of (base_control, subcontrol) in canonical form
|
|
@@ -1192,7 +1466,21 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
1192
1466
|
if not m:
|
|
1193
1467
|
return cid.upper(), None
|
|
1194
1468
|
base = m.group(1).upper()
|
|
1469
|
+
# Normalize leading zeros in base control number (e.g., AC-01 -> AC-1)
|
|
1470
|
+
if "-" in base:
|
|
1471
|
+
prefix, number = base.split("-", 1)
|
|
1472
|
+
try:
|
|
1473
|
+
normalized_number = str(int(number))
|
|
1474
|
+
base = f"{prefix}-{normalized_number}"
|
|
1475
|
+
except ValueError:
|
|
1476
|
+
pass # Keep original if conversion fails
|
|
1195
1477
|
sub = m.group(2) or m.group(3) or m.group(4)
|
|
1478
|
+
# Normalize leading zeros in subcontrol (e.g., 01 -> 1)
|
|
1479
|
+
if sub:
|
|
1480
|
+
try:
|
|
1481
|
+
sub = str(int(sub))
|
|
1482
|
+
except ValueError:
|
|
1483
|
+
pass # Keep original if conversion fails
|
|
1196
1484
|
return base, sub
|
|
1197
1485
|
|
|
1198
1486
|
def _create_control_assessment(
|
|
@@ -1370,9 +1658,198 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
1370
1658
|
|
|
1371
1659
|
return "\n".join(html_parts)
|
|
1372
1660
|
|
|
1661
|
+
def _get_security_plan(self) -> Optional[regscale_models.SecurityPlan]:
|
|
1662
|
+
"""
|
|
1663
|
+
Get the security plan for this integration.
|
|
1664
|
+
|
|
1665
|
+
:return: SecurityPlan instance or None
|
|
1666
|
+
:rtype: Optional[regscale_models.SecurityPlan]
|
|
1667
|
+
"""
|
|
1668
|
+
if self._security_plan is None:
|
|
1669
|
+
try:
|
|
1670
|
+
logger.debug(f"Retrieving security plan with ID: {self.plan_id}")
|
|
1671
|
+
self._security_plan = SecurityPlan.get_object(object_id=self.plan_id)
|
|
1672
|
+
if self._security_plan:
|
|
1673
|
+
logger.debug(f"Retrieved security plan: {self._security_plan.title}")
|
|
1674
|
+
logger.debug(f"complianceSettingsId: {getattr(self._security_plan, 'complianceSettingsId', None)}")
|
|
1675
|
+
else:
|
|
1676
|
+
logger.debug(f"No security plan found with ID: {self.plan_id}")
|
|
1677
|
+
except Exception as e:
|
|
1678
|
+
logger.debug(f"Error getting security plan {self.plan_id}: {e}")
|
|
1679
|
+
self._security_plan = None
|
|
1680
|
+
return self._security_plan
|
|
1681
|
+
|
|
1682
|
+
def _get_compliance_settings(self) -> Optional[regscale_models.ComplianceSettings]:
|
|
1683
|
+
"""
|
|
1684
|
+
Get compliance settings for the security plan.
|
|
1685
|
+
|
|
1686
|
+
:return: ComplianceSettings instance or None
|
|
1687
|
+
:rtype: Optional[regscale_models.ComplianceSettings]
|
|
1688
|
+
"""
|
|
1689
|
+
if self._compliance_settings is None:
|
|
1690
|
+
try:
|
|
1691
|
+
security_plan = self._get_security_plan()
|
|
1692
|
+
if self._has_valid_compliance_settings_id(security_plan):
|
|
1693
|
+
self._compliance_settings = self._fetch_compliance_settings(security_plan)
|
|
1694
|
+
else:
|
|
1695
|
+
self._log_missing_compliance_settings_reason(security_plan)
|
|
1696
|
+
except Exception as e:
|
|
1697
|
+
logger.debug(f"Error getting compliance settings: {e}")
|
|
1698
|
+
import traceback
|
|
1699
|
+
|
|
1700
|
+
logger.debug(f"Full traceback: {traceback.format_exc()}")
|
|
1701
|
+
self._compliance_settings = None
|
|
1702
|
+
return self._compliance_settings
|
|
1703
|
+
|
|
1704
|
+
def _has_valid_compliance_settings_id(self, security_plan) -> bool:
|
|
1705
|
+
"""Check if security plan has valid compliance settings ID."""
|
|
1706
|
+
return security_plan and hasattr(security_plan, "complianceSettingsId") and security_plan.complianceSettingsId
|
|
1707
|
+
|
|
1708
|
+
def _fetch_compliance_settings(self, security_plan) -> Optional[regscale_models.ComplianceSettings]:
|
|
1709
|
+
"""Fetch and log compliance settings."""
|
|
1710
|
+
logger.debug(f"Retrieving compliance settings with ID: {security_plan.complianceSettingsId}")
|
|
1711
|
+
compliance_settings = ComplianceSettings.get_object(object_id=security_plan.complianceSettingsId)
|
|
1712
|
+
|
|
1713
|
+
if compliance_settings:
|
|
1714
|
+
logger.debug(f"Using compliance settings: {compliance_settings.title}")
|
|
1715
|
+
logger.debug(
|
|
1716
|
+
f"Compliance settings has field groups: {bool(getattr(compliance_settings, 'complianceSettingsFieldGroups', None))}"
|
|
1717
|
+
)
|
|
1718
|
+
else:
|
|
1719
|
+
logger.debug(f"No compliance settings found for ID: {security_plan.complianceSettingsId}")
|
|
1720
|
+
|
|
1721
|
+
return compliance_settings
|
|
1722
|
+
|
|
1723
|
+
def _log_missing_compliance_settings_reason(self, security_plan) -> None:
|
|
1724
|
+
"""Log specific reason why compliance settings are not available."""
|
|
1725
|
+
if not security_plan:
|
|
1726
|
+
logger.debug("Security plan not found")
|
|
1727
|
+
elif not hasattr(security_plan, "complianceSettingsId"):
|
|
1728
|
+
logger.debug("Security plan does not have complianceSettingsId attribute")
|
|
1729
|
+
elif not security_plan.complianceSettingsId:
|
|
1730
|
+
logger.debug("Security plan has no complianceSettingsId set")
|
|
1731
|
+
|
|
1732
|
+
def _get_implementation_status_from_result(self, result: str) -> str:
|
|
1733
|
+
"""
|
|
1734
|
+
Get implementation status based on assessment result using compliance settings.
|
|
1735
|
+
|
|
1736
|
+
:param str result: Assessment result ('Pass' or 'Fail')
|
|
1737
|
+
:return: Implementation status string
|
|
1738
|
+
:rtype: str
|
|
1739
|
+
"""
|
|
1740
|
+
logger.debug(f"Getting implementation status for result: {result}")
|
|
1741
|
+
compliance_settings = self._get_compliance_settings()
|
|
1742
|
+
|
|
1743
|
+
if compliance_settings:
|
|
1744
|
+
logger.debug(f"Using compliance settings: {compliance_settings.title}")
|
|
1745
|
+
try:
|
|
1746
|
+
status_labels = compliance_settings.get_field_labels("implementationStatus")
|
|
1747
|
+
logger.debug(f"Available status labels: {status_labels}")
|
|
1748
|
+
|
|
1749
|
+
best_match = self._find_best_status_match(result.lower(), status_labels)
|
|
1750
|
+
if best_match:
|
|
1751
|
+
return best_match
|
|
1752
|
+
|
|
1753
|
+
logger.debug(f"No matching compliance setting found for result: {result}")
|
|
1754
|
+
except Exception as e:
|
|
1755
|
+
logger.debug(f"Error using compliance settings for status mapping: {e}")
|
|
1756
|
+
else:
|
|
1757
|
+
logger.debug("No compliance settings available, using default mapping")
|
|
1758
|
+
|
|
1759
|
+
return self._get_default_status(result)
|
|
1760
|
+
|
|
1761
|
+
def _find_best_status_match(self, result_lower: str, status_labels: list) -> Optional[str]:
|
|
1762
|
+
"""Find best matching status label for the given result."""
|
|
1763
|
+
best_match = None
|
|
1764
|
+
best_match_score = 0
|
|
1765
|
+
|
|
1766
|
+
for label in status_labels:
|
|
1767
|
+
label_lower = label.lower()
|
|
1768
|
+
logger.debug(f"Checking label '{label}' for result '{result_lower}'")
|
|
1769
|
+
|
|
1770
|
+
if result_lower == "pass":
|
|
1771
|
+
score = self._score_pass_result_label(label_lower)
|
|
1772
|
+
elif result_lower == "fail":
|
|
1773
|
+
score = self._score_fail_result_label(label_lower)
|
|
1774
|
+
else:
|
|
1775
|
+
score = 0
|
|
1776
|
+
|
|
1777
|
+
if score > best_match_score:
|
|
1778
|
+
best_match = label
|
|
1779
|
+
best_match_score = score
|
|
1780
|
+
logger.debug(f"New best match: '{label}' (score: {score})")
|
|
1781
|
+
|
|
1782
|
+
if best_match:
|
|
1783
|
+
logger.debug(
|
|
1784
|
+
f"Selected best match: '{best_match}' (final score: {best_match_score}) for {result_lower} result"
|
|
1785
|
+
)
|
|
1786
|
+
|
|
1787
|
+
return best_match
|
|
1788
|
+
|
|
1789
|
+
def _score_pass_result_label(self, label_lower: str) -> int:
|
|
1790
|
+
"""Score a label for Pass results (higher score = better match)."""
|
|
1791
|
+
# Skip negative keywords that shouldn't match Pass results
|
|
1792
|
+
negative_keywords = ["not", "failed", "violation", "remediation", "unsatisfied", "non-compliant"]
|
|
1793
|
+
if any(neg_kw in label_lower for neg_kw in negative_keywords):
|
|
1794
|
+
return 0
|
|
1795
|
+
|
|
1796
|
+
# Exact word matches get highest priority
|
|
1797
|
+
exact_matches = {"implemented": 100, "complete": 95, "compliant": 90, "satisfied": 85}
|
|
1798
|
+
if label_lower in exact_matches:
|
|
1799
|
+
return exact_matches[label_lower]
|
|
1800
|
+
|
|
1801
|
+
# Partial matches get lower priority
|
|
1802
|
+
if "implemented" in label_lower and "fully" not in label_lower and "partially" not in label_lower:
|
|
1803
|
+
return 80
|
|
1804
|
+
elif "complete" in label_lower:
|
|
1805
|
+
return 75
|
|
1806
|
+
elif "compliant" in label_lower:
|
|
1807
|
+
return 70
|
|
1808
|
+
elif "satisfied" in label_lower:
|
|
1809
|
+
return 65
|
|
1810
|
+
elif "fully" in label_lower and "implemented" in label_lower:
|
|
1811
|
+
return 60
|
|
1812
|
+
elif "partially" in label_lower and "implemented" in label_lower:
|
|
1813
|
+
return 55
|
|
1814
|
+
elif "implemented" in label_lower:
|
|
1815
|
+
return 45
|
|
1816
|
+
elif any(kw in label_lower for kw in ["complete", "compliant", "satisfied"]):
|
|
1817
|
+
return 40
|
|
1818
|
+
|
|
1819
|
+
return 0
|
|
1820
|
+
|
|
1821
|
+
def _score_fail_result_label(self, label_lower: str) -> int:
|
|
1822
|
+
"""Score a label for Fail results (higher score = better match)."""
|
|
1823
|
+
# Exact word matches get highest priority
|
|
1824
|
+
if label_lower in ["remediation", "in remediation"]:
|
|
1825
|
+
return 100
|
|
1826
|
+
elif label_lower in ["failed", "not implemented"]:
|
|
1827
|
+
return 95
|
|
1828
|
+
elif label_lower in ["non-compliant", "violation"]:
|
|
1829
|
+
return 90
|
|
1830
|
+
elif label_lower == "unsatisfied":
|
|
1831
|
+
return 85
|
|
1832
|
+
|
|
1833
|
+
# Partial matches get lower priority
|
|
1834
|
+
elif "remediation" in label_lower:
|
|
1835
|
+
return 80
|
|
1836
|
+
elif "failed" in label_lower or "not implemented" in label_lower:
|
|
1837
|
+
return 75
|
|
1838
|
+
elif any(kw in label_lower for kw in ["non-compliant", "violation", "unsatisfied"]):
|
|
1839
|
+
return 70
|
|
1840
|
+
|
|
1841
|
+
return 0
|
|
1842
|
+
|
|
1843
|
+
def _get_default_status(self, result: str) -> str:
|
|
1844
|
+
"""Get default implementation status when no compliance settings are available."""
|
|
1845
|
+
default_status = "Fully Implemented" if result.lower() == "pass" else "In Remediation"
|
|
1846
|
+
logger.debug(f"Using default status: {default_status}")
|
|
1847
|
+
return default_status
|
|
1848
|
+
|
|
1373
1849
|
def _update_implementation_status(self, implementation: ControlImplementation, result: str) -> None:
|
|
1374
1850
|
"""
|
|
1375
1851
|
Update control implementation status based on assessment result.
|
|
1852
|
+
Uses compliance settings from the security plan if available, otherwise falls back to defaults.
|
|
1376
1853
|
|
|
1377
1854
|
:param ControlImplementation implementation: Control implementation to update
|
|
1378
1855
|
:param str result: Assessment result ('Pass' or 'Fail')
|
|
@@ -1380,10 +1857,8 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
1380
1857
|
:rtype: None
|
|
1381
1858
|
"""
|
|
1382
1859
|
try:
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
else:
|
|
1386
|
-
new_status = "In Remediation"
|
|
1860
|
+
# Get status from compliance settings or fallback to default
|
|
1861
|
+
new_status = self._get_implementation_status_from_result(result)
|
|
1387
1862
|
|
|
1388
1863
|
# Update implementation status
|
|
1389
1864
|
implementation.status = new_status
|
|
@@ -1401,7 +1876,7 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
1401
1876
|
objective.status = new_status
|
|
1402
1877
|
objective.save()
|
|
1403
1878
|
|
|
1404
|
-
logger.debug(f"Updated implementation status to {new_status}")
|
|
1879
|
+
logger.debug(f"Updated implementation status to {new_status} (from compliance settings)")
|
|
1405
1880
|
|
|
1406
1881
|
except Exception as e:
|
|
1407
1882
|
logger.error(f"Error updating implementation status: {e}")
|
|
@@ -1568,17 +2043,18 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
1568
2043
|
|
|
1569
2044
|
# Check for existing issue by external_id first
|
|
1570
2045
|
external_id = finding.external_id
|
|
2046
|
+
logger.debug(f"Looking for existing issue with external_id: '{external_id}'")
|
|
1571
2047
|
existing_issue = self._find_existing_issue_cached(external_id)
|
|
1572
2048
|
|
|
1573
2049
|
if existing_issue:
|
|
1574
2050
|
logger.debug(
|
|
1575
|
-
f"Found existing issue {existing_issue.id} for external_id {external_id}, updating instead of creating"
|
|
2051
|
+
f"Found existing issue {existing_issue.id} (other_identifier: '{existing_issue.otherIdentifier}') for lookup external_id '{external_id}', updating instead of creating"
|
|
1576
2052
|
)
|
|
1577
2053
|
|
|
1578
2054
|
# Update existing issue with new finding data
|
|
1579
2055
|
existing_issue.title = title
|
|
1580
2056
|
existing_issue.description = finding.description
|
|
1581
|
-
existing_issue.
|
|
2057
|
+
existing_issue.severityLevel = finding.severity
|
|
1582
2058
|
existing_issue.status = finding.status
|
|
1583
2059
|
# Ensure affectedControls is updated from the finding's control id
|
|
1584
2060
|
try:
|
|
@@ -1598,6 +2074,8 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
1598
2074
|
except Exception:
|
|
1599
2075
|
pass
|
|
1600
2076
|
existing_issue.dateLastUpdated = self.scan_date
|
|
2077
|
+
# Set organization ID based on Issue Owner or SSP Owner hierarchy
|
|
2078
|
+
existing_issue.orgId = self.determine_issue_organization_id(existing_issue.issueOwnerId)
|
|
1601
2079
|
existing_issue.save()
|
|
1602
2080
|
|
|
1603
2081
|
return existing_issue
|