regscale-cli 6.25.1.0__py3-none-any.whl → 6.26.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 (80) hide show
  1. regscale/_version.py +1 -1
  2. regscale/airflow/hierarchy.py +2 -2
  3. regscale/core/app/application.py +18 -3
  4. regscale/core/app/internal/login.py +0 -1
  5. regscale/core/app/utils/catalog_utils/common.py +1 -1
  6. regscale/integrations/commercial/sicura/api.py +14 -13
  7. regscale/integrations/commercial/sicura/commands.py +8 -2
  8. regscale/integrations/commercial/sicura/scanner.py +49 -39
  9. regscale/integrations/commercial/stigv2/ckl_parser.py +5 -5
  10. regscale/integrations/commercial/wizv2/click.py +26 -26
  11. regscale/integrations/commercial/wizv2/compliance_report.py +152 -157
  12. regscale/integrations/commercial/wizv2/scanner.py +3 -3
  13. regscale/integrations/compliance_integration.py +67 -2
  14. regscale/integrations/control_matcher.py +358 -0
  15. regscale/integrations/milestone_manager.py +291 -0
  16. regscale/integrations/public/__init__.py +1 -0
  17. regscale/integrations/public/cci_importer.py +37 -38
  18. regscale/integrations/public/fedramp/click.py +60 -2
  19. regscale/integrations/public/fedramp/poam_export_v5.py +888 -0
  20. regscale/integrations/scanner_integration.py +150 -96
  21. regscale/models/integration_models/cisa_kev_data.json +154 -4
  22. regscale/models/integration_models/nexpose.py +36 -10
  23. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  24. regscale/models/locking.py +12 -8
  25. regscale/models/platform.py +1 -2
  26. regscale/models/regscale_models/control_implementation.py +46 -21
  27. regscale/models/regscale_models/issue.py +256 -94
  28. regscale/models/regscale_models/milestone.py +1 -1
  29. regscale/models/regscale_models/regscale_model.py +6 -1
  30. regscale/templates/__init__.py +0 -0
  31. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.26.0.0.dist-info}/METADATA +1 -1
  32. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.26.0.0.dist-info}/RECORD +80 -33
  33. tests/regscale/integrations/commercial/__init__.py +0 -0
  34. tests/regscale/integrations/commercial/conftest.py +28 -0
  35. tests/regscale/integrations/commercial/microsoft_defender/__init__.py +1 -0
  36. tests/regscale/integrations/commercial/microsoft_defender/test_defender.py +1517 -0
  37. tests/regscale/integrations/commercial/microsoft_defender/test_defender_api.py +1748 -0
  38. tests/regscale/integrations/commercial/microsoft_defender/test_defender_constants.py +327 -0
  39. tests/regscale/integrations/commercial/microsoft_defender/test_defender_scanner.py +487 -0
  40. tests/regscale/integrations/commercial/test_aws.py +3731 -0
  41. tests/regscale/integrations/commercial/test_burp.py +48 -0
  42. tests/regscale/integrations/commercial/test_crowdstrike.py +49 -0
  43. tests/regscale/integrations/commercial/test_dependabot.py +341 -0
  44. tests/regscale/integrations/commercial/test_gcp.py +1543 -0
  45. tests/regscale/integrations/commercial/test_gitlab.py +549 -0
  46. tests/regscale/integrations/commercial/test_ip_mac_address_length.py +84 -0
  47. tests/regscale/integrations/commercial/test_jira.py +1814 -0
  48. tests/regscale/integrations/commercial/test_npm_audit.py +42 -0
  49. tests/regscale/integrations/commercial/test_okta.py +1228 -0
  50. tests/regscale/integrations/commercial/test_sarif_converter.py +251 -0
  51. tests/regscale/integrations/commercial/test_sicura.py +350 -0
  52. tests/regscale/integrations/commercial/test_snow.py +423 -0
  53. tests/regscale/integrations/commercial/test_sonarcloud.py +394 -0
  54. tests/regscale/integrations/commercial/test_sqlserver.py +186 -0
  55. tests/regscale/integrations/commercial/test_stig.py +33 -0
  56. tests/regscale/integrations/commercial/test_stig_mapper.py +153 -0
  57. tests/regscale/integrations/commercial/test_stigv2.py +406 -0
  58. tests/regscale/integrations/commercial/test_wiz.py +1469 -0
  59. tests/regscale/integrations/commercial/test_wiz_inventory.py +256 -0
  60. tests/regscale/integrations/commercial/wizv2/__init__.py +339 -0
  61. tests/regscale/integrations/commercial/wizv2/test_compliance_report_normalization.py +138 -0
  62. tests/regscale/integrations/commercial/wizv2/test_issue.py +343 -0
  63. tests/regscale/integrations/commercial/wizv2/test_wiz_click_client_id.py +165 -0
  64. tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_report.py +1351 -0
  65. tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_unit.py +341 -0
  66. tests/regscale/integrations/commercial/wizv2/test_wiz_control_normalization.py +138 -0
  67. tests/regscale/integrations/commercial/wizv2/test_wiz_policy_compliance.py +750 -0
  68. tests/regscale/integrations/commercial/wizv2/test_wiz_status_mapping.py +149 -0
  69. tests/regscale/integrations/commercial/wizv2/test_wizv2.py +264 -0
  70. tests/regscale/integrations/commercial/wizv2/test_wizv2_utils.py +624 -0
  71. tests/regscale/integrations/public/fedramp/__init__.py +1 -0
  72. tests/regscale/integrations/public/fedramp/test_poam_export_v5.py +1293 -0
  73. tests/regscale/integrations/test_control_matcher.py +1314 -0
  74. tests/regscale/integrations/test_control_matching.py +155 -0
  75. tests/regscale/integrations/test_milestone_manager.py +408 -0
  76. tests/regscale/models/test_issue.py +378 -1
  77. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.26.0.0.dist-info}/LICENSE +0 -0
  78. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.26.0.0.dist-info}/WHEEL +0 -0
  79. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.26.0.0.dist-info}/entry_points.txt +0 -0
  80. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.26.0.0.dist-info}/top_level.txt +0 -0
@@ -17,8 +17,10 @@ from regscale.integrations.commercial.wizv2.reports import WizReportManager
17
17
  from regscale.integrations.commercial.wizv2.variables import WizVariables
18
18
  from regscale.integrations.commercial.wizv2.wiz_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.
@@ -783,7 +794,7 @@ class WizComplianceReportProcessor(ComplianceIntegration):
783
794
  """
784
795
  try:
785
796
  # Filter for compliance reports for this specific project
786
- filter_by = {"project": [self.wiz_project_id], "type": ["COMPLIANCE_ASSESSMENTS"]}
797
+ filter_by = {"projectId": [self.wiz_project_id], "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)
@@ -876,76 +887,66 @@ class WizComplianceReportProcessor(ComplianceIntegration):
876
887
  """
877
888
  Update passing controls to 'Implemented' status in RegScale.
878
889
 
890
+ Uses ControlMatcher for robust control ID matching with leading zero normalization.
891
+
879
892
  :param list[str] passing_control_ids: List of control IDs that passed
880
893
  """
881
894
  if not passing_control_ids:
882
895
  return
883
896
 
884
- # Initialize Application for control implementation updates
885
- # app = Application() # Will be used through self.app
886
-
887
897
  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
898
  logger.debug(f"Looking for passing control IDs: {passing_control_ids}")
901
899
 
902
900
  # Prepare batch updates for passing controls
903
901
  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]}")
902
+ controls_not_found = []
908
903
 
909
904
  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}")
905
+ # Use ControlMatcher to find implementation with robust control ID matching
906
+ impl = self._control_matcher.find_control_implementation(
907
+ control_id=control_id, parent_id=self.plan_id, parent_module=self.parent_module
908
+ )
909
+
910
+ if impl:
911
+ logger.debug(f"Found matching implementation for '{control_id}': {impl.id}")
912
+
913
+ # Update status using compliance settings
914
+ new_status = self._get_implementation_status_from_result("Pass")
915
+ logger.debug(f"Setting control {control_id} status from 'Pass' result to: {new_status}")
916
+ impl.status = new_status
917
+ impl.dateLastAssessed = get_current_datetime()
918
+ impl.lastAssessmentResult = "Pass"
919
+ impl.bStatusImplemented = True
920
+
921
+ # Ensure required fields are set if empty
922
+ if not impl.responsibility:
923
+ impl.responsibility = ControlImplementation.get_default_responsibility(parent_id=impl.parentId)
924
+ logger.debug(f"Setting default responsibility for control {control_id}: {impl.responsibility}")
925
+
926
+ if not impl.implementation:
927
+ impl.implementation = f"Implementation details for {control_id} will be documented."
928
+ logger.debug(f"Setting default implementation statement for control {control_id}")
929
+
930
+ # Set audit fields if available
931
+ user_id = self.app.config.get("userId")
932
+ if user_id:
933
+ impl.lastUpdatedById = user_id
934
+ impl.dateLastUpdated = get_current_datetime()
935
+
936
+ implementations_to_update.append(impl.dict())
937
+ logger.info(f"Marking control {control_id} as {new_status}")
938
+ else:
939
+ logger.debug(f"Control '{control_id}' not found in implementation map")
940
+ controls_not_found.append(control_id)
941
+
942
+ # Log summary
943
+ if controls_not_found:
944
+ logger.info(f"Passing control IDs not found in plan: {', '.join(sorted(controls_not_found))}")
945
+
946
+ logger.info(
947
+ f"Control implementation status update summary: {len(implementations_to_update)} found, "
948
+ f"{len(controls_not_found)} not in plan"
949
+ )
949
950
 
950
951
  # Batch update all implementations
951
952
  if implementations_to_update:
@@ -957,104 +958,47 @@ class WizComplianceReportProcessor(ComplianceIntegration):
957
958
  except Exception as e:
958
959
  logger.error(f"Error updating control implementation status: {e}")
959
960
 
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
961
+ def _prepare_failing_control_update(self, control_id: str) -> Optional[dict]:
967
962
  """
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 {}
963
+ Prepare a single failing control for update.
996
964
 
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
965
+ :param str control_id: Control ID to update
966
+ :return: Dictionary representation of updated implementation, or None if not found
1037
967
  :rtype: Optional[dict]
1038
968
  """
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}")
969
+ impl = self._control_matcher.find_control_implementation(
970
+ control_id=control_id, parent_id=self.plan_id, parent_module=self.parent_module
971
+ )
1043
972
 
1044
- impl = ControlImplementation.get_object(object_id=impl_id)
1045
973
  if not impl:
1046
- logger.warning(f"Could not retrieve implementation object for ID {impl_id}")
974
+ logger.debug(f"Control '{control_id}' not found in implementation map")
1047
975
  return None
1048
976
 
1049
- # Update status using compliance settings
977
+ logger.debug(f"Found matching implementation for '{control_id}': {impl.id}")
978
+
1050
979
  new_status = self._get_implementation_status_from_result("Fail")
1051
980
  logger.debug(f"Setting control {control_id} status from 'Fail' result to: {new_status}")
981
+
1052
982
  impl.status = new_status
1053
983
  impl.dateLastAssessed = get_current_datetime()
1054
984
  impl.lastAssessmentResult = "Fail"
1055
985
  impl.bStatusImplemented = False
1056
986
 
1057
- # Ensure required fields are set if empty
987
+ self._set_default_fields_if_empty(impl, control_id)
988
+ self._set_audit_fields(impl)
989
+
990
+ logger.info(f"Marking control {control_id} as {new_status}")
991
+ return impl.dict()
992
+
993
+ def _set_default_fields_if_empty(self, impl: ControlImplementation, control_id: str) -> None:
994
+ """
995
+ Set default values for required fields if they are empty.
996
+
997
+ :param ControlImplementation impl: Implementation to update
998
+ :param str control_id: Control ID for logging
999
+ :return: None
1000
+ :rtype: None
1001
+ """
1058
1002
  if not impl.responsibility:
1059
1003
  impl.responsibility = ControlImplementation.get_default_responsibility(parent_id=impl.parentId)
1060
1004
  logger.debug(f"Setting default responsibility for control {control_id}: {impl.responsibility}")
@@ -1063,27 +1007,76 @@ class WizComplianceReportProcessor(ComplianceIntegration):
1063
1007
  impl.implementation = f"Implementation details for {control_id} will be documented."
1064
1008
  logger.debug(f"Setting default implementation statement for control {control_id}")
1065
1009
 
1066
- # Set audit fields if available
1010
+ def _set_audit_fields(self, impl: ControlImplementation) -> None:
1011
+ """
1012
+ Set audit fields on implementation if user ID is available.
1013
+
1014
+ :param ControlImplementation impl: Implementation to update
1015
+ :return: None
1016
+ :rtype: None
1017
+ """
1067
1018
  user_id = self.app.config.get("userId")
1068
1019
  if user_id:
1069
1020
  impl.lastUpdatedById = user_id
1070
1021
  impl.dateLastUpdated = get_current_datetime()
1071
1022
 
1072
- logger.info(f"Marking control {control_id} as {new_status}")
1073
- return impl.dict()
1023
+ def _update_failing_controls_to_in_remediation(self, control_ids: List[str]) -> None:
1024
+ """
1025
+ Update control implementation status to In Remediation for failing controls.
1026
+
1027
+ Uses ControlMatcher for robust control ID matching with leading zero normalization.
1028
+
1029
+ :param List[str] control_ids: List of control IDs that are failing
1030
+ :return: None
1031
+ :rtype: None
1032
+ """
1033
+ if not control_ids:
1034
+ return
1074
1035
 
1075
- def _log_update_summary(self, implementations_to_update: list, controls_not_found: list) -> None:
1076
- """Log summary of control implementation updates."""
1036
+ try:
1037
+ logger.debug(f"Looking for failing control IDs: {control_ids}")
1038
+
1039
+ implementations_to_update = []
1040
+ controls_not_found = []
1041
+
1042
+ for control_id in control_ids:
1043
+ impl_dict = self._prepare_failing_control_update(control_id)
1044
+ if impl_dict:
1045
+ implementations_to_update.append(impl_dict)
1046
+ else:
1047
+ controls_not_found.append(control_id)
1048
+
1049
+ self._log_update_summary(controls_not_found, implementations_to_update)
1050
+ self._batch_update_implementations(implementations_to_update)
1051
+
1052
+ except Exception as e:
1053
+ logger.error(f"Error updating failing control implementation status: {e}")
1054
+
1055
+ def _log_update_summary(self, controls_not_found: List[str], implementations_to_update: List[dict]) -> None:
1056
+ """
1057
+ Log summary of control update operation.
1058
+
1059
+ :param List[str] controls_not_found: List of controls not found
1060
+ :param List[dict] implementations_to_update: List of implementations to update
1061
+ :return: None
1062
+ :rtype: None
1063
+ """
1077
1064
  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
- )
1065
+ logger.info(f"Control IDs not found in plan: {', '.join(sorted(controls_not_found))}")
1066
+
1067
+ logger.info(
1068
+ f"Control implementation status update summary: {len(implementations_to_update)} found, "
1069
+ f"{len(controls_not_found)} not in plan"
1070
+ )
1071
+
1072
+ def _batch_update_implementations(self, implementations_to_update: List[dict]) -> None:
1073
+ """
1074
+ Perform batch update of control implementations.
1084
1075
 
1085
- def _batch_update_implementations(self, implementations_to_update: list) -> None:
1086
- """Perform batch update of control implementations."""
1076
+ :param List[dict] implementations_to_update: List of implementations to update
1077
+ :return: None
1078
+ :rtype: None
1079
+ """
1087
1080
  if implementations_to_update:
1088
1081
  ControlImplementation.put_batch_implementation(self.app, implementations_to_update)
1089
1082
  logger.debug(f"Updated {len(implementations_to_update)} Control Implementations, Successfully!")
@@ -1551,6 +1544,7 @@ class WizComplianceReportProcessor(ComplianceIntegration):
1551
1544
  rule_id=control_id,
1552
1545
  baseline=representative_item.framework,
1553
1546
  affected_controls=control_id,
1547
+ identification=IssueIdentification.SecurityControlAssessment.value,
1554
1548
  )
1555
1549
 
1556
1550
  def _create_finding_from_compliance_item(self, compliance_item: ComplianceItem) -> Optional[Any]:
@@ -1587,6 +1581,7 @@ class WizComplianceReportProcessor(ComplianceIntegration):
1587
1581
  rule_id=compliance_item.control_id,
1588
1582
  baseline=compliance_item.framework,
1589
1583
  affected_controls=compliance_item.affected_controls, # Use our property with all control IDs
1584
+ identification=IssueIdentification.SecurityControlAssessment.value,
1590
1585
  )
1591
1586
 
1592
1587
  return finding
@@ -1940,10 +1940,9 @@ class WizVulnerabilityIntegration(ScannerIntegration):
1940
1940
  else:
1941
1941
  logger.info("File %s does not exist. Fetching new data...", file_path)
1942
1942
 
1943
- self.authenticate(WizVariables.wizClientId, WizVariables.wizClientSecret)
1944
-
1943
+ # Ensure we have a valid token (should already be set by caller)
1945
1944
  if not self.wiz_token:
1946
- error_and_exit("Wiz token is not set. Please authenticate first.")
1945
+ error_and_exit("Wiz token is not set. Please authenticate before calling fetch_wiz_data_if_needed.")
1947
1946
 
1948
1947
  nodes = fetch_wiz_data(
1949
1948
  query=query,
@@ -1952,6 +1951,7 @@ class WizVulnerabilityIntegration(ScannerIntegration):
1952
1951
  token=self.wiz_token,
1953
1952
  topic_key=topic_key,
1954
1953
  )
1954
+
1955
1955
  with open(file_path, "w", encoding="utf-8") as file:
1956
1956
  json.dump(nodes, file)
1957
1957
 
@@ -13,6 +13,7 @@ from collections import defaultdict
13
13
  from typing import Dict, List, Optional, Any, Iterator
14
14
 
15
15
  from regscale.core.app.utils.app_utils import get_current_datetime, regscale_string_to_datetime
16
+ from regscale.integrations.control_matcher import ControlMatcher
16
17
  from regscale.integrations.scanner_integration import (
17
18
  ScannerIntegration,
18
19
  IntegrationAsset,
@@ -166,6 +167,9 @@ class ComplianceIntegration(ScannerIntegration, ABC):
166
167
  self._compliance_settings = None
167
168
  self._security_plan = None
168
169
 
170
+ # Initialize control matcher for robust control ID matching
171
+ self._control_matcher = ControlMatcher()
172
+
169
173
  def is_poam(self, finding: IntegrationFinding) -> bool: # type: ignore[override]
170
174
  """
171
175
  Determines if an issue should be considered a POAM for compliance integrations.
@@ -1245,13 +1249,22 @@ class ComplianceIntegration(ScannerIntegration, ABC):
1245
1249
  """
1246
1250
  Find matching implementation and security control for a control ID.
1247
1251
 
1252
+ Uses ControlMatcher for robust control ID matching with leading zero normalization.
1253
+
1248
1254
  :param str control_id: Control identifier to match
1249
1255
  :param List[ControlImplementation] implementations: Available implementations
1250
1256
  :return: Tuple of matching implementation and security control, or (None, None)
1251
1257
  :rtype: tuple[Optional[ControlImplementation], Optional[SecurityControl]]
1252
1258
  """
1259
+ # Generate all variations of the search control ID for matching
1260
+ search_variations = self._control_matcher._get_control_id_variations(control_id)
1261
+ if not search_variations:
1262
+ logger.debug(f"Could not generate control ID variations for: {control_id}")
1263
+ return None, None
1264
+
1253
1265
  matching_implementation = None
1254
1266
  matching_security_control = None
1267
+
1255
1268
  for implementation in implementations:
1256
1269
  try:
1257
1270
  security_control = SecurityControl.get_object(object_id=implementation.controlID)
@@ -1264,10 +1277,16 @@ class ComplianceIntegration(ScannerIntegration, ABC):
1264
1277
  if not security_control_id:
1265
1278
  logger.debug(f"Security control {security_control.id} has no controlId")
1266
1279
  continue
1280
+
1281
+ # Get variations of the security control ID
1282
+ control_variations = self._control_matcher._get_control_id_variations(security_control_id)
1283
+
1267
1284
  logger.debug(
1268
- f"Comparing extracted '{control_id}' with RegScale control '{security_control_id}' (impl: {implementation.id})"
1285
+ f"Comparing control '{control_id}' variations {list(search_variations)[:3]} with RegScale control '{security_control_id}' variations {list(control_variations)[:3]} (impl: {implementation.id})"
1269
1286
  )
1270
- if self._control_ids_match(control_id, security_control_id):
1287
+
1288
+ # Check if any variation matches (set intersection)
1289
+ if search_variations & control_variations:
1271
1290
  matching_implementation = implementation
1272
1291
  matching_security_control = security_control
1273
1292
  logger.info(
@@ -1364,12 +1383,19 @@ class ComplianceIntegration(ScannerIntegration, ABC):
1364
1383
  component_title = f"Control {sec_control.controlId}"
1365
1384
  component = self.components_by_title.get(component_title) if hasattr(self, "components_by_title") else None
1366
1385
  if not component:
1386
+ # Get complianceSettingsId from the security plan
1387
+ security_plan = self._get_security_plan()
1388
+ compliance_settings_id = getattr(security_plan, "complianceSettingsId", None) if security_plan else None
1389
+ if not compliance_settings_id:
1390
+ compliance_setting = self._get_compliance_settings()
1391
+ compliance_settings_id = getattr(compliance_setting, "id", None) if compliance_setting else None
1367
1392
  component = regscale_models.Component(
1368
1393
  title=component_title,
1369
1394
  componentType=regscale_models.ComponentType.Hardware,
1370
1395
  securityPlansId=self.plan_id,
1371
1396
  description=component_title,
1372
1397
  componentOwnerId=self.get_assessor_id(),
1398
+ complianceSettingsId=compliance_settings_id,
1373
1399
  ).get_or_create()
1374
1400
  regscale_models.ComponentMapping(
1375
1401
  componentId=component.id,
@@ -2050,6 +2076,8 @@ class ComplianceIntegration(ScannerIntegration, ABC):
2050
2076
  """
2051
2077
  Create or update an issue from a finding, using cache to prevent duplicates.
2052
2078
 
2079
+ Properly handles milestone creation for compliance integrations.
2080
+
2053
2081
  :param str title: Issue title
2054
2082
  :param IntegrationFinding finding: The finding to create issue from
2055
2083
  :return: Created or updated issue
@@ -2068,6 +2096,9 @@ class ComplianceIntegration(ScannerIntegration, ABC):
2068
2096
  f"Found existing issue {existing_issue.id} (other_identifier: '{existing_issue.otherIdentifier}') for lookup external_id '{external_id}', updating instead of creating"
2069
2097
  )
2070
2098
 
2099
+ # Store original status for milestone comparison
2100
+ original_status = existing_issue.status
2101
+
2071
2102
  # Update existing issue with new finding data
2072
2103
  existing_issue.title = title
2073
2104
  existing_issue.description = finding.description
@@ -2095,8 +2126,42 @@ class ComplianceIntegration(ScannerIntegration, ABC):
2095
2126
  existing_issue.orgId = self.determine_issue_organization_id(existing_issue.issueOwnerId)
2096
2127
  existing_issue.save()
2097
2128
 
2129
+ # Create milestone if status changed
2130
+ # Reconstruct original issue state for comparison
2131
+ original_issue = regscale_models.Issue()
2132
+ original_issue.status = original_status
2133
+ self._create_milestones_for_updated_issue(existing_issue, finding, original_issue)
2134
+
2098
2135
  return existing_issue
2099
2136
  else:
2100
2137
  # No existing issue found, create new one using parent method
2101
2138
  logger.debug(f"No existing issue found for external_id {external_id}, creating new issue")
2102
2139
  return super().create_or_update_issue_from_finding(title, finding)
2140
+
2141
+ def _create_milestones_for_updated_issue(
2142
+ self,
2143
+ issue: regscale_models.Issue,
2144
+ finding: IntegrationFinding,
2145
+ original_issue: regscale_models.Issue,
2146
+ ) -> None:
2147
+ """
2148
+ Create milestones for an updated issue in compliance integration.
2149
+
2150
+ This method handles both status transition milestones and backfilling of missing
2151
+ creation milestones for existing issues.
2152
+
2153
+ :param regscale_models.Issue issue: The updated issue
2154
+ :param IntegrationFinding finding: The finding data
2155
+ :param regscale_models.Issue original_issue: Original state for comparison
2156
+ """
2157
+ milestone_manager = self.get_milestone_manager()
2158
+
2159
+ # First, ensure the issue has a creation milestone (backfill if missing)
2160
+ milestone_manager.ensure_creation_milestone_exists(issue=issue, finding=finding)
2161
+
2162
+ # Then, handle status transition milestones
2163
+ milestone_manager.create_milestones_for_issue(
2164
+ issue=issue,
2165
+ finding=finding,
2166
+ existing_issue=original_issue,
2167
+ )