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.
- regscale/_version.py +1 -1
- regscale/airflow/hierarchy.py +2 -2
- regscale/core/app/application.py +18 -3
- regscale/core/app/internal/login.py +0 -1
- regscale/core/app/utils/catalog_utils/common.py +1 -1
- regscale/integrations/commercial/sicura/api.py +14 -13
- regscale/integrations/commercial/sicura/commands.py +8 -2
- regscale/integrations/commercial/sicura/scanner.py +49 -39
- regscale/integrations/commercial/stigv2/ckl_parser.py +5 -5
- regscale/integrations/commercial/wizv2/click.py +26 -26
- regscale/integrations/commercial/wizv2/compliance_report.py +152 -157
- regscale/integrations/commercial/wizv2/scanner.py +3 -3
- regscale/integrations/compliance_integration.py +67 -2
- regscale/integrations/control_matcher.py +358 -0
- regscale/integrations/milestone_manager.py +291 -0
- regscale/integrations/public/__init__.py +1 -0
- regscale/integrations/public/cci_importer.py +37 -38
- regscale/integrations/public/fedramp/click.py +60 -2
- regscale/integrations/public/fedramp/poam_export_v5.py +888 -0
- regscale/integrations/scanner_integration.py +150 -96
- regscale/models/integration_models/cisa_kev_data.json +154 -4
- regscale/models/integration_models/nexpose.py +36 -10
- regscale/models/integration_models/synqly_models/capabilities.json +1 -1
- regscale/models/locking.py +12 -8
- regscale/models/platform.py +1 -2
- regscale/models/regscale_models/control_implementation.py +46 -21
- regscale/models/regscale_models/issue.py +256 -94
- regscale/models/regscale_models/milestone.py +1 -1
- regscale/models/regscale_models/regscale_model.py +6 -1
- regscale/templates/__init__.py +0 -0
- {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.26.0.0.dist-info}/METADATA +1 -1
- {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.26.0.0.dist-info}/RECORD +80 -33
- tests/regscale/integrations/commercial/__init__.py +0 -0
- tests/regscale/integrations/commercial/conftest.py +28 -0
- tests/regscale/integrations/commercial/microsoft_defender/__init__.py +1 -0
- tests/regscale/integrations/commercial/microsoft_defender/test_defender.py +1517 -0
- tests/regscale/integrations/commercial/microsoft_defender/test_defender_api.py +1748 -0
- tests/regscale/integrations/commercial/microsoft_defender/test_defender_constants.py +327 -0
- tests/regscale/integrations/commercial/microsoft_defender/test_defender_scanner.py +487 -0
- tests/regscale/integrations/commercial/test_aws.py +3731 -0
- tests/regscale/integrations/commercial/test_burp.py +48 -0
- tests/regscale/integrations/commercial/test_crowdstrike.py +49 -0
- tests/regscale/integrations/commercial/test_dependabot.py +341 -0
- tests/regscale/integrations/commercial/test_gcp.py +1543 -0
- tests/regscale/integrations/commercial/test_gitlab.py +549 -0
- tests/regscale/integrations/commercial/test_ip_mac_address_length.py +84 -0
- tests/regscale/integrations/commercial/test_jira.py +1814 -0
- tests/regscale/integrations/commercial/test_npm_audit.py +42 -0
- tests/regscale/integrations/commercial/test_okta.py +1228 -0
- tests/regscale/integrations/commercial/test_sarif_converter.py +251 -0
- tests/regscale/integrations/commercial/test_sicura.py +350 -0
- tests/regscale/integrations/commercial/test_snow.py +423 -0
- tests/regscale/integrations/commercial/test_sonarcloud.py +394 -0
- tests/regscale/integrations/commercial/test_sqlserver.py +186 -0
- tests/regscale/integrations/commercial/test_stig.py +33 -0
- tests/regscale/integrations/commercial/test_stig_mapper.py +153 -0
- tests/regscale/integrations/commercial/test_stigv2.py +406 -0
- tests/regscale/integrations/commercial/test_wiz.py +1469 -0
- tests/regscale/integrations/commercial/test_wiz_inventory.py +256 -0
- tests/regscale/integrations/commercial/wizv2/__init__.py +339 -0
- tests/regscale/integrations/commercial/wizv2/test_compliance_report_normalization.py +138 -0
- tests/regscale/integrations/commercial/wizv2/test_issue.py +343 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_click_client_id.py +165 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_report.py +1351 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_unit.py +341 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_control_normalization.py +138 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_policy_compliance.py +750 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_status_mapping.py +149 -0
- tests/regscale/integrations/commercial/wizv2/test_wizv2.py +264 -0
- tests/regscale/integrations/commercial/wizv2/test_wizv2_utils.py +624 -0
- tests/regscale/integrations/public/fedramp/__init__.py +1 -0
- tests/regscale/integrations/public/fedramp/test_poam_export_v5.py +1293 -0
- tests/regscale/integrations/test_control_matcher.py +1314 -0
- tests/regscale/integrations/test_control_matching.py +155 -0
- tests/regscale/integrations/test_milestone_manager.py +408 -0
- tests/regscale/models/test_issue.py +378 -1
- {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.26.0.0.dist-info}/LICENSE +0 -0
- {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.26.0.0.dist-info}/WHEEL +0 -0
- {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.26.0.0.dist-info}/entry_points.txt +0 -0
- {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
|
|
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
|
-
|
|
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 = {"
|
|
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
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
998
|
-
return
|
|
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
|
-
|
|
1040
|
-
|
|
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.
|
|
974
|
+
logger.debug(f"Control '{control_id}' not found in implementation map")
|
|
1047
975
|
return None
|
|
1048
976
|
|
|
1049
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1073
|
-
|
|
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
|
-
|
|
1076
|
-
|
|
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
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
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
|
-
|
|
1086
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
+
)
|