regscale-cli 6.25.1.0__py3-none-any.whl → 6.27.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of regscale-cli might be problematic. Click here for more details.

Files changed (146) hide show
  1. regscale/_version.py +1 -1
  2. regscale/airflow/hierarchy.py +2 -2
  3. regscale/core/app/application.py +19 -4
  4. regscale/core/app/internal/evidence.py +419 -2
  5. regscale/core/app/internal/login.py +0 -1
  6. regscale/core/app/utils/catalog_utils/common.py +1 -1
  7. regscale/dev/code_gen.py +24 -20
  8. regscale/integrations/commercial/jira.py +367 -126
  9. regscale/integrations/commercial/qualys/__init__.py +7 -8
  10. regscale/integrations/commercial/qualys/scanner.py +8 -3
  11. regscale/integrations/commercial/sicura/api.py +14 -13
  12. regscale/integrations/commercial/sicura/commands.py +8 -2
  13. regscale/integrations/commercial/sicura/scanner.py +49 -39
  14. regscale/integrations/commercial/stigv2/ckl_parser.py +5 -5
  15. regscale/integrations/commercial/synqly/assets.py +17 -0
  16. regscale/integrations/commercial/synqly/vulnerabilities.py +45 -28
  17. regscale/integrations/commercial/tenablev2/cis_parsers.py +453 -0
  18. regscale/integrations/commercial/tenablev2/cis_scanner.py +447 -0
  19. regscale/integrations/commercial/tenablev2/commands.py +142 -1
  20. regscale/integrations/commercial/tenablev2/scanner.py +0 -1
  21. regscale/integrations/commercial/tenablev2/stig_parsers.py +113 -57
  22. regscale/integrations/commercial/wizv2/WizDataMixin.py +1 -1
  23. regscale/integrations/commercial/wizv2/click.py +64 -79
  24. regscale/integrations/commercial/wizv2/compliance/__init__.py +15 -0
  25. regscale/integrations/commercial/wizv2/{policy_compliance_helpers.py → compliance/helpers.py} +78 -60
  26. regscale/integrations/commercial/wizv2/compliance_report.py +161 -165
  27. regscale/integrations/commercial/wizv2/core/__init__.py +133 -0
  28. regscale/integrations/commercial/wizv2/{async_client.py → core/client.py} +3 -3
  29. regscale/integrations/commercial/wizv2/{constants.py → core/constants.py} +1 -17
  30. regscale/integrations/commercial/wizv2/core/file_operations.py +237 -0
  31. regscale/integrations/commercial/wizv2/fetchers/__init__.py +11 -0
  32. regscale/integrations/commercial/wizv2/{data_fetcher.py → fetchers/policy_assessment.py} +5 -9
  33. regscale/integrations/commercial/wizv2/issue.py +1 -1
  34. regscale/integrations/commercial/wizv2/models/__init__.py +0 -0
  35. regscale/integrations/commercial/wizv2/parsers/__init__.py +34 -0
  36. regscale/integrations/commercial/wizv2/{parsers.py → parsers/main.py} +1 -1
  37. regscale/integrations/commercial/wizv2/processors/__init__.py +11 -0
  38. regscale/integrations/commercial/wizv2/{finding_processor.py → processors/finding.py} +1 -1
  39. regscale/integrations/commercial/wizv2/reports.py +1 -1
  40. regscale/integrations/commercial/wizv2/sbom.py +1 -1
  41. regscale/integrations/commercial/wizv2/scanner.py +39 -99
  42. regscale/integrations/commercial/wizv2/utils/__init__.py +48 -0
  43. regscale/integrations/commercial/wizv2/{utils.py → utils/main.py} +116 -61
  44. regscale/integrations/commercial/wizv2/variables.py +89 -3
  45. regscale/integrations/compliance_integration.py +60 -41
  46. regscale/integrations/control_matcher.py +377 -0
  47. regscale/integrations/due_date_handler.py +14 -8
  48. regscale/integrations/milestone_manager.py +291 -0
  49. regscale/integrations/public/__init__.py +1 -0
  50. regscale/integrations/public/cci_importer.py +37 -38
  51. regscale/integrations/public/fedramp/click.py +60 -2
  52. regscale/integrations/public/fedramp/docx_parser.py +10 -1
  53. regscale/integrations/public/fedramp/fedramp_cis_crm.py +393 -340
  54. regscale/integrations/public/fedramp/fedramp_five.py +1 -1
  55. regscale/integrations/public/fedramp/poam_export_v5.py +888 -0
  56. regscale/integrations/scanner_integration.py +277 -153
  57. regscale/models/integration_models/cisa_kev_data.json +282 -9
  58. regscale/models/integration_models/nexpose.py +36 -10
  59. regscale/models/integration_models/qualys.py +3 -4
  60. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  61. regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +24 -7
  62. regscale/models/integration_models/synqly_models/synqly_model.py +8 -1
  63. regscale/models/locking.py +12 -8
  64. regscale/models/platform.py +1 -2
  65. regscale/models/regscale_models/control_implementation.py +47 -22
  66. regscale/models/regscale_models/issue.py +256 -95
  67. regscale/models/regscale_models/milestone.py +1 -1
  68. regscale/models/regscale_models/regscale_model.py +6 -1
  69. regscale/templates/__init__.py +0 -0
  70. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/METADATA +1 -17
  71. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/RECORD +145 -65
  72. tests/regscale/integrations/commercial/__init__.py +0 -0
  73. tests/regscale/integrations/commercial/conftest.py +28 -0
  74. tests/regscale/integrations/commercial/microsoft_defender/__init__.py +1 -0
  75. tests/regscale/integrations/commercial/microsoft_defender/test_defender.py +1517 -0
  76. tests/regscale/integrations/commercial/microsoft_defender/test_defender_api.py +1748 -0
  77. tests/regscale/integrations/commercial/microsoft_defender/test_defender_constants.py +327 -0
  78. tests/regscale/integrations/commercial/microsoft_defender/test_defender_scanner.py +487 -0
  79. tests/regscale/integrations/commercial/test_aws.py +3731 -0
  80. tests/regscale/integrations/commercial/test_burp.py +48 -0
  81. tests/regscale/integrations/commercial/test_crowdstrike.py +49 -0
  82. tests/regscale/integrations/commercial/test_dependabot.py +341 -0
  83. tests/regscale/integrations/commercial/test_gcp.py +1543 -0
  84. tests/regscale/integrations/commercial/test_gitlab.py +549 -0
  85. tests/regscale/integrations/commercial/test_ip_mac_address_length.py +84 -0
  86. tests/regscale/integrations/commercial/test_jira.py +2204 -0
  87. tests/regscale/integrations/commercial/test_npm_audit.py +42 -0
  88. tests/regscale/integrations/commercial/test_okta.py +1228 -0
  89. tests/regscale/integrations/commercial/test_sarif_converter.py +251 -0
  90. tests/regscale/integrations/commercial/test_sicura.py +350 -0
  91. tests/regscale/integrations/commercial/test_snow.py +423 -0
  92. tests/regscale/integrations/commercial/test_sonarcloud.py +394 -0
  93. tests/regscale/integrations/commercial/test_sqlserver.py +186 -0
  94. tests/regscale/integrations/commercial/test_stig.py +33 -0
  95. tests/regscale/integrations/commercial/test_stig_mapper.py +153 -0
  96. tests/regscale/integrations/commercial/test_stigv2.py +406 -0
  97. tests/regscale/integrations/commercial/test_wiz.py +1365 -0
  98. tests/regscale/integrations/commercial/test_wiz_inventory.py +256 -0
  99. tests/regscale/integrations/commercial/wizv2/__init__.py +339 -0
  100. tests/regscale/integrations/commercial/wizv2/compliance/__init__.py +1 -0
  101. tests/regscale/integrations/commercial/wizv2/compliance/test_helpers.py +903 -0
  102. tests/regscale/integrations/commercial/wizv2/core/__init__.py +1 -0
  103. tests/regscale/integrations/commercial/wizv2/core/test_auth.py +701 -0
  104. tests/regscale/integrations/commercial/wizv2/core/test_client.py +1037 -0
  105. tests/regscale/integrations/commercial/wizv2/core/test_file_operations.py +989 -0
  106. tests/regscale/integrations/commercial/wizv2/fetchers/__init__.py +1 -0
  107. tests/regscale/integrations/commercial/wizv2/fetchers/test_policy_assessment.py +805 -0
  108. tests/regscale/integrations/commercial/wizv2/parsers/__init__.py +1 -0
  109. tests/regscale/integrations/commercial/wizv2/parsers/test_main.py +1153 -0
  110. tests/regscale/integrations/commercial/wizv2/processors/__init__.py +1 -0
  111. tests/regscale/integrations/commercial/wizv2/processors/test_finding.py +671 -0
  112. tests/regscale/integrations/commercial/wizv2/test_WizDataMixin.py +537 -0
  113. tests/regscale/integrations/commercial/wizv2/test_click_comprehensive.py +851 -0
  114. tests/regscale/integrations/commercial/wizv2/test_compliance_report_comprehensive.py +910 -0
  115. tests/regscale/integrations/commercial/wizv2/test_compliance_report_normalization.py +138 -0
  116. tests/regscale/integrations/commercial/wizv2/test_file_cleanup.py +283 -0
  117. tests/regscale/integrations/commercial/wizv2/test_file_operations.py +260 -0
  118. tests/regscale/integrations/commercial/wizv2/test_issue.py +343 -0
  119. tests/regscale/integrations/commercial/wizv2/test_issue_comprehensive.py +1203 -0
  120. tests/regscale/integrations/commercial/wizv2/test_reports.py +497 -0
  121. tests/regscale/integrations/commercial/wizv2/test_sbom.py +643 -0
  122. tests/regscale/integrations/commercial/wizv2/test_scanner_comprehensive.py +805 -0
  123. tests/regscale/integrations/commercial/wizv2/test_wiz_click_client_id.py +165 -0
  124. tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_report.py +1394 -0
  125. tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_unit.py +341 -0
  126. tests/regscale/integrations/commercial/wizv2/test_wiz_control_normalization.py +138 -0
  127. tests/regscale/integrations/commercial/wizv2/test_wiz_findings_comprehensive.py +364 -0
  128. tests/regscale/integrations/commercial/wizv2/test_wiz_inventory_comprehensive.py +644 -0
  129. tests/regscale/integrations/commercial/wizv2/test_wiz_status_mapping.py +149 -0
  130. tests/regscale/integrations/commercial/wizv2/test_wizv2.py +1132 -0
  131. tests/regscale/integrations/commercial/wizv2/test_wizv2_utils.py +519 -0
  132. tests/regscale/integrations/commercial/wizv2/utils/__init__.py +1 -0
  133. tests/regscale/integrations/commercial/wizv2/utils/test_main.py +1523 -0
  134. tests/regscale/integrations/public/fedramp/__init__.py +1 -0
  135. tests/regscale/integrations/public/fedramp/test_poam_export_v5.py +1293 -0
  136. tests/regscale/integrations/public/test_fedramp.py +301 -0
  137. tests/regscale/integrations/test_control_matcher.py +1397 -0
  138. tests/regscale/integrations/test_control_matching.py +155 -0
  139. tests/regscale/integrations/test_milestone_manager.py +408 -0
  140. tests/regscale/models/test_issue.py +378 -1
  141. regscale/integrations/commercial/wizv2/policy_compliance.py +0 -3543
  142. /regscale/integrations/commercial/wizv2/{wiz_auth.py → core/auth.py} +0 -0
  143. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/LICENSE +0 -0
  144. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/WHEEL +0 -0
  145. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/entry_points.txt +0 -0
  146. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/top_level.txt +0 -0
@@ -266,32 +266,39 @@ class IssueFieldSetter:
266
266
  )
267
267
 
268
268
  for impl in implementations:
269
- if not hasattr(impl, "controlID") or not impl.controlID:
270
- continue
271
-
272
- # Check cache for security control
273
- security_control = self.cache.get_security_control(impl.controlID)
274
- if not security_control:
275
- security_control = regscale_models.SecurityControl.get_object(object_id=impl.controlID)
276
- if security_control:
277
- self.cache.set_security_control(impl.controlID, security_control)
278
-
279
- if security_control and hasattr(security_control, "controlId"):
280
- from regscale.integrations.commercial.wizv2.policy_compliance import WizPolicyComplianceIntegration
281
-
282
- impl_control_id = WizPolicyComplianceIntegration._normalize_control_id_string(
283
- security_control.controlId
284
- )
285
-
286
- if impl_control_id == control_id:
287
- logger.debug(f"✓ Found control implementation {impl.id} for control {control_id}")
288
- return impl.id
269
+ if impl_id := self._check_implementation_match(impl, control_id):
270
+ return impl_id
289
271
 
290
272
  return None
291
273
  except Exception as e:
292
274
  logger.error(f"Error finding control implementation for {control_id}: {e}")
293
275
  return None
294
276
 
277
+ def _check_implementation_match(self, impl, control_id: str) -> Optional[int]:
278
+ """Check if implementation matches the control ID."""
279
+ if not hasattr(impl, "controlID") or not impl.controlID:
280
+ return None
281
+
282
+ # Check cache for security control
283
+ security_control = self.cache.get_security_control(impl.controlID)
284
+ if not security_control:
285
+ security_control = regscale_models.SecurityControl.get_object(object_id=impl.controlID)
286
+ if security_control:
287
+ self.cache.set_security_control(impl.controlID, security_control)
288
+
289
+ if not security_control or not hasattr(security_control, "controlId"):
290
+ return None
291
+
292
+ from regscale.integrations.commercial.wizv2.policy_compliance import WizPolicyComplianceIntegration
293
+
294
+ impl_control_id = WizPolicyComplianceIntegration._normalize_control_id_string(security_control.controlId)
295
+
296
+ if impl_control_id == control_id:
297
+ logger.debug(f"✓ Found control implementation {impl.id} for control {control_id}")
298
+ return impl.id
299
+
300
+ return None
301
+
295
302
  def _get_or_find_assessment_id(self, impl_id: int) -> Optional[int]:
296
303
  """
297
304
  Get assessment ID from cache or database.
@@ -480,22 +487,26 @@ class ControlAssessmentProcessor:
480
487
  assessments = regscale_models.Assessment.get_all_by_parent(parent_id=impl_id, parent_module="controls")
481
488
 
482
489
  for assessment in assessments:
483
- if hasattr(assessment, "actualFinish") and assessment.actualFinish:
484
- try:
485
- if isinstance(assessment.actualFinish, str):
486
- assessment_date = regscale_string_to_datetime(assessment.actualFinish).date()
487
- elif hasattr(assessment.actualFinish, "date"):
488
- assessment_date = assessment.actualFinish.date()
489
- else:
490
- assessment_date = assessment.actualFinish
490
+ if assessment_date := self._get_assessment_date(assessment):
491
+ if assessment_date == today:
492
+ self.cache.set_assessment(impl_id, assessment)
493
+ return assessment
491
494
 
492
- if assessment_date == today:
493
- self.cache.set_assessment(impl_id, assessment)
494
- return assessment
495
- except Exception:
496
- continue
495
+ return None
496
+ except Exception:
497
+ return None
497
498
 
499
+ def _get_assessment_date(self, assessment):
500
+ """Extract date from assessment actualFinish field."""
501
+ if not hasattr(assessment, "actualFinish") or not assessment.actualFinish:
498
502
  return None
503
+
504
+ try:
505
+ if isinstance(assessment.actualFinish, str):
506
+ return regscale_string_to_datetime(assessment.actualFinish).date()
507
+ if hasattr(assessment.actualFinish, "date"):
508
+ return assessment.actualFinish.date()
509
+ return assessment.actualFinish
499
510
  except Exception:
500
511
  return None
501
512
 
@@ -511,8 +522,14 @@ class ControlAssessmentProcessor:
511
522
  result_color = "#d32f2f" if result == "Fail" else "#2e7d32"
512
523
  bg_color = "#ffebee" if result == "Fail" else "#e8f5e8"
513
524
 
514
- html_parts = [
515
- f"""
525
+ header_html = self._create_report_header(control_id, result, result_color, bg_color, len(compliance_items))
526
+ summary_html = self._create_report_summary(compliance_items) if compliance_items else ""
527
+
528
+ return "\n".join([header_html, summary_html])
529
+
530
+ def _create_report_header(self, control_id: str, result: str, result_color: str, bg_color: str, total: int) -> str:
531
+ """Create HTML header section for assessment report."""
532
+ return f"""
516
533
  <div style="margin-bottom: 20px; padding: 15px; border: 2px solid {result_color};
517
534
  border-radius: 5px; background-color: {bg_color};">
518
535
  <h3 style="margin: 0 0 10px 0; color: {result_color};">
@@ -522,34 +539,24 @@ class ControlAssessmentProcessor:
522
539
  <span style="color: {result_color}; font-weight: bold;">{result}</span></p>
523
540
  <p><strong>Assessment Date:</strong> {self.scan_date}</p>
524
541
  <p><strong>Framework:</strong> {self.framework}</p>
525
- <p><strong>Total Policy Assessments:</strong> {len(compliance_items)}</p>
542
+ <p><strong>Total Policy Assessments:</strong> {total}</p>
526
543
  </div>
527
544
  """
528
- ]
529
-
530
- if compliance_items:
531
- pass_count = len(
532
- [
533
- item
534
- for item in compliance_items
535
- if hasattr(item, "compliance_result")
536
- and item.compliance_result in ["PASS", "PASSED", "pass", "passed"]
537
- ]
538
- )
539
- fail_count = len(compliance_items) - pass_count
540
545
 
541
- unique_resources = set()
542
- unique_policies = set()
546
+ def _create_report_summary(self, compliance_items: List[Any]) -> str:
547
+ """Create HTML summary section for assessment report."""
548
+ pass_count = len(
549
+ [
550
+ item
551
+ for item in compliance_items
552
+ if hasattr(item, "compliance_result") and item.compliance_result in ["PASS", "PASSED", "pass", "passed"]
553
+ ]
554
+ )
555
+ fail_count = len(compliance_items) - pass_count
543
556
 
544
- for item in compliance_items:
545
- if hasattr(item, "resource_id"):
546
- unique_resources.add(item.resource_id)
547
- if hasattr(item, "description") and item.description:
548
- policy_desc = item.description[:50] + "..." if len(item.description) > 50 else item.description
549
- unique_policies.add(policy_desc)
557
+ unique_resources, unique_policies = self._extract_unique_items(compliance_items)
550
558
 
551
- html_parts.append(
552
- f"""
559
+ return f"""
553
560
  <div style="margin-top: 20px;">
554
561
  <h4>Assessment Summary</h4>
555
562
  <p><strong>Policy Assessments:</strong> {len(compliance_items)} total</p>
@@ -559,6 +566,17 @@ class ControlAssessmentProcessor:
559
566
  <p><strong>Failing:</strong> <span style="color: #d32f2f;">{fail_count}</span></p>
560
567
  </div>
561
568
  """
562
- )
563
569
 
564
- return "\n".join(html_parts)
570
+ def _extract_unique_items(self, compliance_items: List[Any]):
571
+ """Extract unique resources and policies from compliance items."""
572
+ unique_resources = set()
573
+ unique_policies = set()
574
+
575
+ for item in compliance_items:
576
+ if hasattr(item, "resource_id"):
577
+ unique_resources.add(item.resource_id)
578
+ if hasattr(item, "description") and item.description:
579
+ policy_desc = item.description[:50] + "..." if len(item.description) > 50 else item.description
580
+ unique_policies.add(policy_desc)
581
+
582
+ return unique_resources, unique_policies
@@ -15,10 +15,12 @@ from regscale.core.app.utils.app_utils import get_current_datetime
15
15
  from regscale.integrations.commercial.wizv2.file_cleanup import ReportFileCleanup
16
16
  from regscale.integrations.commercial.wizv2.reports import WizReportManager
17
17
  from regscale.integrations.commercial.wizv2.variables import WizVariables
18
- from regscale.integrations.commercial.wizv2.wiz_auth import wiz_authenticate
18
+ from regscale.integrations.commercial.wizv2.core.auth import wiz_authenticate
19
19
  from regscale.integrations.compliance_integration import ComplianceIntegration, ComplianceItem
20
+ from regscale.integrations.control_matcher import ControlMatcher
20
21
  from regscale.models import regscale_models
21
- from regscale.models.regscale_models.control_implementation import ControlImplementation, ControlImplementationStatus
22
+ from regscale.models.regscale_models.control_implementation import ControlImplementation
23
+ from regscale.models.regscale_models.issue import IssueIdentification
22
24
 
23
25
  logger = logging.getLogger("regscale")
24
26
 
@@ -135,7 +137,12 @@ class WizComplianceReportItem(ComplianceItem):
135
137
  def _format_control_id(self, base_control: str, enhancement: str) -> str:
136
138
  """Format control ID with optional enhancement."""
137
139
  if enhancement:
138
- return f"{base_control}({enhancement})"
140
+ # Normalize enhancement number to remove leading zeros
141
+ try:
142
+ normalized_enhancement = str(int(enhancement))
143
+ except ValueError:
144
+ normalized_enhancement = enhancement
145
+ return f"{base_control}({normalized_enhancement})"
139
146
  else:
140
147
  return base_control
141
148
 
@@ -277,6 +284,10 @@ class WizComplianceReportProcessor(ComplianceIntegration):
277
284
 
278
285
  self.report_manager = WizReportManager(WizVariables.wizUrl, access_token)
279
286
 
287
+ # Initialize control matcher for robust control ID matching (inherited from parent but ensure it's set)
288
+ if not hasattr(self, "_control_matcher"):
289
+ self._control_matcher = ControlMatcher()
290
+
280
291
  def parse_csv_report(self, file_path: str) -> List[WizComplianceReportItem]:
281
292
  """
282
293
  Parse CSV compliance report.
@@ -782,25 +793,26 @@ class WizComplianceReportProcessor(ComplianceIntegration):
782
793
  :rtype: Optional[str]
783
794
  """
784
795
  try:
785
- # Filter for compliance reports for this specific project
786
- filter_by = {"project": [self.wiz_project_id], "type": ["COMPLIANCE_ASSESSMENTS"]}
796
+ # Filter for compliance reports (projectId not supported in ReportFilters, using name-based lookup)
797
+ filter_by = {"type": ["COMPLIANCE_ASSESSMENTS"]}
787
798
 
788
799
  logger.debug(f"Searching for existing compliance reports with filter: {filter_by}")
789
800
  reports = self.report_manager.list_reports(filter_by=filter_by)
790
801
 
791
802
  if not reports:
792
- logger.info("No existing compliance reports found for this project")
803
+ logger.info("No existing compliance reports found")
793
804
  return None
794
805
 
795
- # Look for reports named "Compliance Report" (the default name)
796
- compliance_reports = [report for report in reports if report.get("name", "").strip() == "Compliance Report"]
806
+ # Look for report with project-specific name
807
+ expected_name = f"Compliance Report - {self.wiz_project_id}"
808
+ matching_reports = [report for report in reports if report.get("name", "").strip() == expected_name]
797
809
 
798
- if not compliance_reports:
799
- logger.info("No compliance reports with standard name found")
810
+ if not matching_reports:
811
+ logger.info(f"No existing compliance report found with name: {expected_name}")
800
812
  return None
801
813
 
802
814
  # Return the first matching report (most recent will be used)
803
- selected_report = compliance_reports[0]
815
+ selected_report = matching_reports[0]
804
816
  report_id = selected_report.get("id")
805
817
  report_name = selected_report.get("name", "Unknown")
806
818
 
@@ -876,76 +888,66 @@ class WizComplianceReportProcessor(ComplianceIntegration):
876
888
  """
877
889
  Update passing controls to 'Implemented' status in RegScale.
878
890
 
891
+ Uses ControlMatcher for robust control ID matching with leading zero normalization.
892
+
879
893
  :param list[str] passing_control_ids: List of control IDs that passed
880
894
  """
881
895
  if not passing_control_ids:
882
896
  return
883
897
 
884
- # Initialize Application for control implementation updates
885
- # app = Application() # Will be used through self.app
886
-
887
898
  try:
888
- # Use the existing method that works for getting control name to implementation ID mapping
889
- control_impl_map = ControlImplementation.get_control_label_map_by_parent(
890
- parent_id=self.plan_id, parent_module=self.parent_module
891
- )
892
-
893
- logger.debug(
894
- f"Built control implementation map with {len(control_impl_map)} entries using get_control_label_map_by_parent"
895
- )
896
- if control_impl_map:
897
- sample_keys = list(control_impl_map.keys())[:10]
898
- logger.debug(f"Sample control names in map: {sample_keys}")
899
-
900
899
  logger.debug(f"Looking for passing control IDs: {passing_control_ids}")
901
900
 
902
901
  # Prepare batch updates for passing controls
903
902
  implementations_to_update = []
904
-
905
- # Debug: Show what keys are actually in the control_impl_map
906
- if control_impl_map:
907
- logger.debug(f"Control implementation map keys: {list(control_impl_map.keys())[:20]}")
903
+ controls_not_found = []
908
904
 
909
905
  for control_id in passing_control_ids:
910
- control_id_lower = control_id.lower()
911
- logger.debug(f"Looking for control '{control_id_lower}' in implementation map")
912
-
913
- if control_id_lower in control_impl_map:
914
- impl_id = control_impl_map[control_id_lower]
915
- logger.debug(f"Found matching implementation for '{control_id_lower}': {impl_id}")
916
-
917
- # Get the ControlImplementation object
918
- impl = ControlImplementation.get_object(object_id=impl_id)
919
- if impl:
920
- # Update status using compliance settings
921
- new_status = self._get_implementation_status_from_result("Pass")
922
- logger.debug(f"Setting control {control_id} status from 'Pass' result to: {new_status}")
923
- impl.status = new_status
924
- impl.dateLastAssessed = get_current_datetime()
925
- impl.lastAssessmentResult = "Pass"
926
- impl.bStatusImplemented = True
927
-
928
- # Ensure required fields are set if empty
929
- if not impl.responsibility:
930
- impl.responsibility = ControlImplementation.get_default_responsibility(
931
- parent_id=impl.parentId
932
- )
933
- logger.debug(
934
- f"Setting default responsibility for control {control_id}: {impl.responsibility}"
935
- )
936
-
937
- if not impl.implementation:
938
- impl.implementation = f"Implementation details for {control_id} will be documented."
939
- logger.debug(f"Setting default implementation statement for control {control_id}")
940
-
941
- # Set audit fields if available
942
- user_id = self.app.config.get("userId")
943
- if user_id:
944
- impl.lastUpdatedById = user_id
945
- impl.dateLastUpdated = get_current_datetime()
946
-
947
- implementations_to_update.append(impl.dict())
948
- logger.info(f"Marking control {control_id} as {new_status}")
906
+ # Use ControlMatcher to find implementation with robust control ID matching
907
+ impl = self._control_matcher.find_control_implementation(
908
+ control_id=control_id, parent_id=self.plan_id, parent_module=self.parent_module
909
+ )
910
+
911
+ if impl:
912
+ logger.debug(f"Found matching implementation for '{control_id}': {impl.id}")
913
+
914
+ # Update status using compliance settings
915
+ new_status = self._get_implementation_status_from_result("Pass")
916
+ logger.debug(f"Setting control {control_id} status from 'Pass' result to: {new_status}")
917
+ impl.status = new_status
918
+ impl.dateLastAssessed = get_current_datetime()
919
+ impl.lastAssessmentResult = "Pass"
920
+ impl.bStatusImplemented = True
921
+
922
+ # Ensure required fields are set if empty
923
+ if not impl.responsibility:
924
+ impl.responsibility = ControlImplementation.get_default_responsibility(parent_id=impl.parentId)
925
+ logger.debug(f"Setting default responsibility for control {control_id}: {impl.responsibility}")
926
+
927
+ if not impl.implementation:
928
+ impl.implementation = f"Implementation details for {control_id} will be documented."
929
+ logger.debug(f"Setting default implementation statement for control {control_id}")
930
+
931
+ # Set audit fields if available
932
+ user_id = self.app.config.get("userId")
933
+ if user_id:
934
+ impl.lastUpdatedById = user_id
935
+ impl.dateLastUpdated = get_current_datetime()
936
+
937
+ implementations_to_update.append(impl.dict())
938
+ logger.info(f"Marking control {control_id} as {new_status}")
939
+ else:
940
+ logger.debug(f"Control '{control_id}' not found in implementation map")
941
+ controls_not_found.append(control_id)
942
+
943
+ # Log summary
944
+ if controls_not_found:
945
+ logger.info(f"Passing control IDs not found in plan: {', '.join(sorted(controls_not_found))}")
946
+
947
+ logger.info(
948
+ f"Control implementation status update summary: {len(implementations_to_update)} found, "
949
+ f"{len(controls_not_found)} not in plan"
950
+ )
949
951
 
950
952
  # Batch update all implementations
951
953
  if implementations_to_update:
@@ -957,104 +959,47 @@ class WizComplianceReportProcessor(ComplianceIntegration):
957
959
  except Exception as e:
958
960
  logger.error(f"Error updating control implementation status: {e}")
959
961
 
960
- def _update_failing_controls_to_in_remediation(self, control_ids: List[str]) -> None:
961
- """
962
- Update control implementation status to In Remediation for failing controls.
963
-
964
- :param List[str] control_ids: List of control IDs that are failing
965
- :return: None
966
- :rtype: None
962
+ def _prepare_failing_control_update(self, control_id: str) -> Optional[dict]:
967
963
  """
968
- if not control_ids:
969
- return
970
-
971
- try:
972
- control_impl_map = self._get_control_implementation_map()
973
- if not control_impl_map:
974
- return
975
-
976
- implementations_to_update, controls_not_found = self._process_failing_control_ids(
977
- control_ids, control_impl_map
978
- )
979
-
980
- self._log_update_summary(implementations_to_update, controls_not_found)
981
- self._batch_update_implementations(implementations_to_update)
982
-
983
- except Exception as e:
984
- logger.error(f"Error updating failing control implementation status: {e}")
985
-
986
- def _get_control_implementation_map(self) -> dict:
987
- """Get control implementation map and validate it exists."""
988
-
989
- control_impl_map = ControlImplementation.get_control_label_map_by_parent(
990
- parent_id=self.plan_id, parent_module=self.parent_module
991
- )
992
-
993
- if not control_impl_map:
994
- logger.warning("No control implementation mapping found for security plan")
995
- return {}
964
+ Prepare a single failing control for update.
996
965
 
997
- logger.debug(f"Control implementation map contains {len(control_impl_map)} entries")
998
- return control_impl_map
999
-
1000
- def _process_failing_control_ids(self, control_ids: List[str], control_impl_map: dict) -> tuple[list, list]:
1001
- """Process failing control IDs and return implementations to update and controls not found."""
1002
-
1003
- logger.debug(f"Looking for failing control IDs: {control_ids}")
1004
- implementations_to_update = []
1005
- controls_not_found = []
1006
-
1007
- # Debug: Show what keys are actually in the control_impl_map for comparison
1008
- if control_impl_map:
1009
- logger.debug(f"Control implementation map keys (first 20): {list(control_impl_map.keys())[:20]}")
1010
-
1011
- for control_id in control_ids:
1012
- control_id_normalized = control_id.lower()
1013
- logger.debug(f"Looking for control '{control_id_normalized}' in implementation map")
1014
-
1015
- if control_id_normalized in control_impl_map:
1016
- impl = self._update_single_control_implementation(
1017
- control_id, control_id_normalized, control_impl_map[control_id_normalized]
1018
- )
1019
- if impl:
1020
- implementations_to_update.append(impl)
1021
- else:
1022
- controls_not_found.append(control_id)
1023
- else:
1024
- logger.debug(f"Control '{control_id_normalized}' not found in implementation map")
1025
- controls_not_found.append(control_id)
1026
-
1027
- return implementations_to_update, controls_not_found
1028
-
1029
- def _update_single_control_implementation(
1030
- self, control_id: str, control_id_normalized: str, impl_id: int
1031
- ) -> Optional[dict]:
1032
- """Update a single control implementation to In Remediation status.
1033
- :param str control_id: ID of the control to update
1034
- :param str control_id_normalized: ID of the control to update
1035
- :param int impl_id: ID of the implementation to update
1036
- :return: Updated implementation status if implementation exists
966
+ :param str control_id: Control ID to update
967
+ :return: Dictionary representation of updated implementation, or None if not found
1037
968
  :rtype: Optional[dict]
1038
969
  """
1039
- from regscale.core.app.utils.app_utils import get_current_datetime
1040
- from regscale.models.regscale_models import ControlImplementationStatus
1041
-
1042
- logger.debug(f"Found matching implementation for '{control_id_normalized}': {impl_id}")
970
+ impl = self._control_matcher.find_control_implementation(
971
+ control_id=control_id, parent_id=self.plan_id, parent_module=self.parent_module
972
+ )
1043
973
 
1044
- impl = ControlImplementation.get_object(object_id=impl_id)
1045
974
  if not impl:
1046
- logger.warning(f"Could not retrieve implementation object for ID {impl_id}")
975
+ logger.debug(f"Control '{control_id}' not found in implementation map")
1047
976
  return None
1048
977
 
1049
- # Update status using compliance settings
978
+ logger.debug(f"Found matching implementation for '{control_id}': {impl.id}")
979
+
1050
980
  new_status = self._get_implementation_status_from_result("Fail")
1051
981
  logger.debug(f"Setting control {control_id} status from 'Fail' result to: {new_status}")
982
+
1052
983
  impl.status = new_status
1053
984
  impl.dateLastAssessed = get_current_datetime()
1054
985
  impl.lastAssessmentResult = "Fail"
1055
986
  impl.bStatusImplemented = False
1056
987
 
1057
- # Ensure required fields are set if empty
988
+ self._set_default_fields_if_empty(impl, control_id)
989
+ self._set_audit_fields(impl)
990
+
991
+ logger.info(f"Marking control {control_id} as {new_status}")
992
+ return impl.dict()
993
+
994
+ def _set_default_fields_if_empty(self, impl: ControlImplementation, control_id: str) -> None:
995
+ """
996
+ Set default values for required fields if they are empty.
997
+
998
+ :param ControlImplementation impl: Implementation to update
999
+ :param str control_id: Control ID for logging
1000
+ :return: None
1001
+ :rtype: None
1002
+ """
1058
1003
  if not impl.responsibility:
1059
1004
  impl.responsibility = ControlImplementation.get_default_responsibility(parent_id=impl.parentId)
1060
1005
  logger.debug(f"Setting default responsibility for control {control_id}: {impl.responsibility}")
@@ -1063,27 +1008,76 @@ class WizComplianceReportProcessor(ComplianceIntegration):
1063
1008
  impl.implementation = f"Implementation details for {control_id} will be documented."
1064
1009
  logger.debug(f"Setting default implementation statement for control {control_id}")
1065
1010
 
1066
- # Set audit fields if available
1011
+ def _set_audit_fields(self, impl: ControlImplementation) -> None:
1012
+ """
1013
+ Set audit fields on implementation if user ID is available.
1014
+
1015
+ :param ControlImplementation impl: Implementation to update
1016
+ :return: None
1017
+ :rtype: None
1018
+ """
1067
1019
  user_id = self.app.config.get("userId")
1068
1020
  if user_id:
1069
1021
  impl.lastUpdatedById = user_id
1070
1022
  impl.dateLastUpdated = get_current_datetime()
1071
1023
 
1072
- logger.info(f"Marking control {control_id} as {new_status}")
1073
- return impl.dict()
1024
+ def _update_failing_controls_to_in_remediation(self, control_ids: List[str]) -> None:
1025
+ """
1026
+ Update control implementation status to In Remediation for failing controls.
1027
+
1028
+ Uses ControlMatcher for robust control ID matching with leading zero normalization.
1029
+
1030
+ :param List[str] control_ids: List of control IDs that are failing
1031
+ :return: None
1032
+ :rtype: None
1033
+ """
1034
+ if not control_ids:
1035
+ return
1074
1036
 
1075
- def _log_update_summary(self, implementations_to_update: list, controls_not_found: list) -> None:
1076
- """Log summary of control implementation updates."""
1037
+ try:
1038
+ logger.debug(f"Looking for failing control IDs: {control_ids}")
1039
+
1040
+ implementations_to_update = []
1041
+ controls_not_found = []
1042
+
1043
+ for control_id in control_ids:
1044
+ impl_dict = self._prepare_failing_control_update(control_id)
1045
+ if impl_dict:
1046
+ implementations_to_update.append(impl_dict)
1047
+ else:
1048
+ controls_not_found.append(control_id)
1049
+
1050
+ self._log_update_summary(controls_not_found, implementations_to_update)
1051
+ self._batch_update_implementations(implementations_to_update)
1052
+
1053
+ except Exception as e:
1054
+ logger.error(f"Error updating failing control implementation status: {e}")
1055
+
1056
+ def _log_update_summary(self, controls_not_found: List[str], implementations_to_update: List[dict]) -> None:
1057
+ """
1058
+ Log summary of control update operation.
1059
+
1060
+ :param List[str] controls_not_found: List of controls not found
1061
+ :param List[dict] implementations_to_update: List of implementations to update
1062
+ :return: None
1063
+ :rtype: None
1064
+ """
1077
1065
  if controls_not_found:
1078
- skipped_list = ", ".join(controls_not_found[:5])
1079
- more_indicator = "..." if len(controls_not_found) > 5 else ""
1080
- logger.info(
1081
- f"Control implementation status update summary: {len(implementations_to_update)} found, "
1082
- f"{len(controls_not_found)} not in plan (skipped: {skipped_list}{more_indicator})"
1083
- )
1066
+ logger.info(f"Control IDs not found in plan: {', '.join(sorted(controls_not_found))}")
1067
+
1068
+ logger.info(
1069
+ f"Control implementation status update summary: {len(implementations_to_update)} found, "
1070
+ f"{len(controls_not_found)} not in plan"
1071
+ )
1072
+
1073
+ def _batch_update_implementations(self, implementations_to_update: List[dict]) -> None:
1074
+ """
1075
+ Perform batch update of control implementations.
1084
1076
 
1085
- def _batch_update_implementations(self, implementations_to_update: list) -> None:
1086
- """Perform batch update of control implementations."""
1077
+ :param List[dict] implementations_to_update: List of implementations to update
1078
+ :return: None
1079
+ :rtype: None
1080
+ """
1087
1081
  if implementations_to_update:
1088
1082
  ControlImplementation.put_batch_implementation(self.app, implementations_to_update)
1089
1083
  logger.debug(f"Updated {len(implementations_to_update)} Control Implementations, Successfully!")
@@ -1551,6 +1545,7 @@ class WizComplianceReportProcessor(ComplianceIntegration):
1551
1545
  rule_id=control_id,
1552
1546
  baseline=representative_item.framework,
1553
1547
  affected_controls=control_id,
1548
+ identification=IssueIdentification.SecurityControlAssessment.value,
1554
1549
  )
1555
1550
 
1556
1551
  def _create_finding_from_compliance_item(self, compliance_item: ComplianceItem) -> Optional[Any]:
@@ -1587,6 +1582,7 @@ class WizComplianceReportProcessor(ComplianceIntegration):
1587
1582
  rule_id=compliance_item.control_id,
1588
1583
  baseline=compliance_item.framework,
1589
1584
  affected_controls=compliance_item.affected_controls, # Use our property with all control IDs
1585
+ identification=IssueIdentification.SecurityControlAssessment.value,
1590
1586
  )
1591
1587
 
1592
1588
  return finding