regscale-cli 6.20.9.1__py3-none-any.whl → 6.20.10.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 (29) hide show
  1. regscale/_version.py +1 -1
  2. regscale/integrations/commercial/defender.py +9 -0
  3. regscale/integrations/commercial/wizv2/async_client.py +325 -0
  4. regscale/integrations/commercial/wizv2/constants.py +756 -0
  5. regscale/integrations/commercial/wizv2/scanner.py +1301 -89
  6. regscale/integrations/commercial/wizv2/utils.py +280 -36
  7. regscale/integrations/commercial/wizv2/variables.py +2 -10
  8. regscale/integrations/scanner_integration.py +58 -2
  9. regscale/integrations/variables.py +1 -0
  10. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  11. regscale/models/regscale_models/__init__.py +13 -0
  12. regscale/models/regscale_models/classification.py +23 -0
  13. regscale/models/regscale_models/cryptography.py +56 -0
  14. regscale/models/regscale_models/deviation.py +4 -4
  15. regscale/models/regscale_models/group.py +3 -2
  16. regscale/models/regscale_models/interconnection.py +1 -1
  17. regscale/models/regscale_models/issue.py +140 -41
  18. regscale/models/regscale_models/milestone.py +40 -0
  19. regscale/models/regscale_models/property.py +0 -1
  20. regscale/models/regscale_models/regscale_model.py +29 -18
  21. regscale/models/regscale_models/team.py +55 -0
  22. {regscale_cli-6.20.9.1.dist-info → regscale_cli-6.20.10.0.dist-info}/METADATA +1 -1
  23. {regscale_cli-6.20.9.1.dist-info → regscale_cli-6.20.10.0.dist-info}/RECORD +29 -23
  24. tests/regscale/integrations/test_property_and_milestone_creation.py +684 -0
  25. tests/regscale/models/test_report.py +105 -29
  26. {regscale_cli-6.20.9.1.dist-info → regscale_cli-6.20.10.0.dist-info}/LICENSE +0 -0
  27. {regscale_cli-6.20.9.1.dist-info → regscale_cli-6.20.10.0.dist-info}/WHEEL +0 -0
  28. {regscale_cli-6.20.9.1.dist-info → regscale_cli-6.20.10.0.dist-info}/entry_points.txt +0 -0
  29. {regscale_cli-6.20.9.1.dist-info → regscale_cli-6.20.10.0.dist-info}/top_level.txt +0 -0
@@ -54,6 +54,7 @@ from regscale.models import (
54
54
  ControlImplementationStatus,
55
55
  ImplementationObjective,
56
56
  )
57
+ from regscale.models.regscale_models.compliance_settings import ComplianceSettings
57
58
  from regscale.utils import PaginatedGraphQLClient
58
59
  from regscale.utils.decorators import deprecated
59
60
 
@@ -781,8 +782,6 @@ def _sync_compliance(
781
782
  fetch_regscale_data_job = compliance_job_progress.add_task(
782
783
  "[#f68d1f]Fetching RegScale Catalog info for framework...", total=1
783
784
  )
784
- compliance_job_progress.update(report_job, completed=True, advance=1)
785
-
786
785
  framework_mapping = {
787
786
  "CSF": "NIST CSF v1.1",
788
787
  "NIST800-53R5": "NIST SP 800-53 Revision 5",
@@ -836,8 +835,14 @@ def _sync_compliance(
836
835
  finally:
837
836
  compliance_job_progress.update(running_compliance_job, advance=1)
838
837
  try:
838
+ controls_with_data = len(controls_to_reports)
839
+ logger.info(f"Creating assessments for {controls_with_data} controls with compliance data")
840
+ if controls_with_data == 0:
841
+ logger.warning("No controls have compliance data from Wiz")
842
+ return report_models
843
+
839
844
  saving_regscale_data_job = compliance_job_progress.add_task(
840
- "[#f68d1f]Saving RegScale data...", total=len(controls_to_reports)
845
+ "[#f68d1f]Saving RegScale data...", total=controls_with_data
841
846
  )
842
847
  create_assessment_from_compliance_report(
843
848
  controls_to_reports=controls_to_reports,
@@ -851,8 +856,8 @@ def _sync_compliance(
851
856
  except Exception:
852
857
  error_message = traceback.format_exc()
853
858
  logger.error(f"Error creating ControlImplementations from compliance report: {error_message}")
854
- finally:
855
- compliance_job_progress.update(saving_regscale_data_job, completed=True, advance=len(controls_to_reports))
859
+ # Re-raise the exception so it's not silently swallowed
860
+ raise
856
861
  return report_models
857
862
 
858
863
 
@@ -932,50 +937,199 @@ def create_assessment_from_compliance_report(
932
937
  :rtype: None
933
938
  """
934
939
  implementations = ControlImplementation.get_all_by_parent(parent_module=regscale_module, parent_id=regscale_id)
940
+ total_controls = len(controls_to_reports)
941
+ processed_count = 0
942
+
935
943
  for control_id, reports in controls_to_reports.items():
936
- control_record_id = None
937
- for control in controls:
938
- if control.get("controlId").lower() == control_id:
939
- control_record_id = control.get("id")
940
- break
941
- filtered_results = [x for x in implementations if x.controlID == control_record_id]
942
- create_report_assessment(filtered_results, reports, control_id)
943
- progress.update(task, advance=1)
944
+ try:
945
+ processed_count += 1
946
+ logger.debug(f"Processing control {control_id} ({processed_count}/{total_controls})")
947
+
948
+ control_record_id = None
949
+ for control in controls:
950
+ if control.get("controlId").lower() == control_id:
951
+ control_record_id = control.get("id")
952
+ break
953
+
954
+ filtered_results = [x for x in implementations if x.controlID == control_record_id]
955
+
956
+ start_time = time.time()
957
+ create_report_assessment(filtered_results, reports, control_id)
958
+ end_time = time.time()
959
+ logger.debug(f"Assessment creation for {control_id} took {end_time - start_time:.2f} seconds")
960
+
961
+ progress.update(task, advance=1)
962
+ logger.debug(f"Updated progress: {processed_count}/{total_controls}")
963
+
964
+ except Exception as e:
965
+ logger.error(f"Error processing control {control_id}: {e}")
966
+ # Still update progress even if there's an error
967
+ progress.update(task, advance=1)
944
968
 
945
969
 
946
970
  def create_report_assessment(filtered_results: List, reports: List, control_id: str) -> None:
947
971
  """
948
- Create report assessment
972
+ Create a single aggregated report assessment per control
949
973
 
950
974
  :param List filtered_results: Filtered results
951
- :param List reports: Reports
975
+ :param List reports: List of ComplianceReport objects for this control
952
976
  :param str control_id: Control ID
953
977
  :return: None
954
978
  :rtype: None
955
979
  """
980
+ logger.debug(f"Creating assessment for control {control_id} with {len(reports)} reports")
981
+
956
982
  implementation = filtered_results[0] if len(filtered_results) > 0 else None
983
+ if not implementation or not reports:
984
+ logger.debug(
985
+ f"Skipping control {control_id}: implementation={bool(implementation)}, reports={len(reports) if reports else 0}"
986
+ )
987
+ return
988
+
989
+ # Aggregate results: Fail if ANY asset fails, Pass only if ALL pass
990
+ overall_result = "Pass"
991
+ pass_count = 0
992
+ fail_count = 0
993
+
994
+ # Collect detailed results for comprehensive reporting
995
+ asset_details = []
996
+
957
997
  for report in reports:
958
- html_summary = format_dict_to_html(report.dict())
959
- if implementation:
960
- a = Assessment(
961
- leadAssessorId=implementation.createdById,
962
- title=f"Wiz compliance report assessment for {control_id}",
963
- assessmentType="Control Testing",
964
- plannedStart=get_current_datetime(),
965
- plannedFinish=get_current_datetime(),
966
- actualFinish=get_current_datetime(),
967
- assessmentResult=report.result,
968
- assessmentReport=html_summary,
969
- status="Complete",
970
- parentId=implementation.id,
971
- parentModule="controls",
972
- isPublic=True,
973
- ).create()
974
- update_implementation_status(
975
- implementation=implementation,
976
- result=report.result,
998
+ if report.result == ComplianceCheckStatus.FAIL.value:
999
+ overall_result = "Fail"
1000
+ fail_count += 1
1001
+ else:
1002
+ pass_count += 1
1003
+
1004
+ # Collect asset details for the report
1005
+ asset_details.append(
1006
+ {
1007
+ "resource_name": report.resource_name,
1008
+ "resource_id": report.resource_id,
1009
+ "cloud_provider": report.cloud_provider,
1010
+ "subscription": report.subscription,
1011
+ "result": report.result,
1012
+ "policy_short_name": report.policy_short_name,
1013
+ "compliance_check": report.compliance_check,
1014
+ "severity": report.severity,
1015
+ "assessed_at": report.assessed_at,
1016
+ }
1017
+ )
1018
+
1019
+ # Create comprehensive HTML summary
1020
+ html_summary = _create_aggregated_assessment_report(
1021
+ control_id=control_id,
1022
+ overall_result=overall_result,
1023
+ pass_count=pass_count,
1024
+ fail_count=fail_count,
1025
+ asset_details=asset_details,
1026
+ total_assets=len(reports),
1027
+ )
1028
+
1029
+ # Create single assessment for this control
1030
+ assessment = Assessment(
1031
+ leadAssessorId=implementation.createdById,
1032
+ title=f"Wiz compliance assessment for {control_id}",
1033
+ assessmentType="Control Testing",
1034
+ plannedStart=get_current_datetime(),
1035
+ plannedFinish=get_current_datetime(),
1036
+ actualFinish=get_current_datetime(),
1037
+ assessmentResult=overall_result,
1038
+ assessmentReport=html_summary,
1039
+ status="Complete",
1040
+ parentId=implementation.id,
1041
+ parentModule="controls",
1042
+ isPublic=True,
1043
+ ).create()
1044
+
1045
+ # Update implementation status once with aggregated result
1046
+ update_implementation_status(
1047
+ implementation=implementation,
1048
+ result=overall_result,
1049
+ )
1050
+
1051
+ logger.info(
1052
+ f"Created aggregated assessment for {control_id}: {assessment.id} "
1053
+ f"(Result: {overall_result}, Assets: {len(reports)}, Pass: {pass_count}, Fail: {fail_count})"
1054
+ )
1055
+
1056
+
1057
+ def _create_aggregated_assessment_report(
1058
+ control_id: str, overall_result: str, pass_count: int, fail_count: int, asset_details: List[Dict], total_assets: int
1059
+ ) -> str:
1060
+ """
1061
+ Create a comprehensive HTML assessment report for aggregated compliance results
1062
+
1063
+ :param str control_id: Control identifier
1064
+ :param str overall_result: Overall Pass/Fail result
1065
+ :param int pass_count: Number of passing assets
1066
+ :param int fail_count: Number of failing assets
1067
+ :param List[Dict] asset_details: Detailed information about each asset
1068
+ :param int total_assets: Total number of assets assessed
1069
+ :return: HTML formatted assessment report
1070
+ :rtype: str
1071
+ """
1072
+ # Create summary section
1073
+ summary_html = f"""
1074
+ <div style="margin-bottom: 20px; padding: 15px; border: 2px solid {'#d32f2f' if overall_result == 'Fail' else '#2e7d32'}; border-radius: 5px; background-color: {'#ffebee' if overall_result == 'Fail' else '#e8f5e8'};">
1075
+ <h3 style="margin: 0 0 10px 0; color: {'#d32f2f' if overall_result == 'Fail' else '#2e7d32'};">
1076
+ Assessment Summary for Control {control_id}
1077
+ </h3>
1078
+ <p><strong>Overall Result:</strong> <span style="color: {'#d32f2f' if overall_result == 'Fail' else '#2e7d32'}; font-weight: bold;">{overall_result}</span></p>
1079
+ <p><strong>Total Assets Assessed:</strong> {total_assets}</p>
1080
+ <p><strong>Passing Assets:</strong> <span style="color: #2e7d32;">{pass_count}</span></p>
1081
+ <p><strong>Failing Assets:</strong> <span style="color: #d32f2f;">{fail_count}</span></p>
1082
+ <p><strong>Assessment Date:</strong> {get_current_datetime()}</p>
1083
+ </div>
1084
+ """
1085
+
1086
+ # Create detailed asset results table
1087
+ if asset_details:
1088
+ table_rows = []
1089
+ for asset in asset_details:
1090
+ result_color = "#d32f2f" if asset["result"] == "Fail" else "#2e7d32"
1091
+ table_rows.append(
1092
+ f"""
1093
+ <tr>
1094
+ <td style="padding: 8px; border-bottom: 1px solid #ddd;">{asset.get('resource_name', 'N/A')}</td>
1095
+ <td style="padding: 8px; border-bottom: 1px solid #ddd;">{asset.get('resource_id', 'N/A')}</td>
1096
+ <td style="padding: 8px; border-bottom: 1px solid #ddd;">{asset.get('cloud_provider', 'N/A')}</td>
1097
+ <td style="padding: 8px; border-bottom: 1px solid #ddd;">{asset.get('subscription', 'N/A')}</td>
1098
+ <td style="padding: 8px; border-bottom: 1px solid #ddd; color: {result_color}; font-weight: bold;">{asset.get('result', 'N/A')}</td>
1099
+ <td style="padding: 8px; border-bottom: 1px solid #ddd;">{asset.get('policy_short_name', 'N/A')}</td>
1100
+ <td style="padding: 8px; border-bottom: 1px solid #ddd;">{asset.get('compliance_check', 'N/A')}</td>
1101
+ <td style="padding: 8px; border-bottom: 1px solid #ddd;">{asset.get('severity', 'N/A')}</td>
1102
+ </tr>
1103
+ """
977
1104
  )
978
- logger.info(f"Created report assessment for {control_id}: {a.id}")
1105
+
1106
+ asset_table_html = f"""
1107
+ <div style="margin-top: 20px;">
1108
+ <h4>Detailed Asset Results</h4>
1109
+ <table style="width: 100%; border-collapse: collapse; border: 1px solid #ddd;">
1110
+ <thead>
1111
+ <tr style="background-color: #f5f5f5;">
1112
+ <th style="padding: 10px; border-bottom: 2px solid #ddd; text-align: left;">Resource Name</th>
1113
+ <th style="padding: 10px; border-bottom: 2px solid #ddd; text-align: left;">Resource ID</th>
1114
+ <th style="padding: 10px; border-bottom: 2px solid #ddd; text-align: left;">Cloud Provider</th>
1115
+ <th style="padding: 10px; border-bottom: 2px solid #ddd; text-align: left;">Subscription</th>
1116
+ <th style="padding: 10px; border-bottom: 2px solid #ddd; text-align: left;">Result</th>
1117
+ <th style="padding: 10px; border-bottom: 2px solid #ddd; text-align: left;">Policy</th>
1118
+ <th style="padding: 10px; border-bottom: 2px solid #ddd; text-align: left;">Compliance Check</th>
1119
+ <th style="padding: 10px; border-bottom: 2px solid #ddd; text-align: left;">Severity</th>
1120
+ </tr>
1121
+ </thead>
1122
+ <tbody>
1123
+ {''.join(table_rows)}
1124
+ </tbody>
1125
+ </table>
1126
+ </div>
1127
+ """
1128
+ else:
1129
+ asset_table_html = "<p><em>No asset details available.</em></p>"
1130
+
1131
+ # Combine summary and details
1132
+ return summary_html + asset_table_html
979
1133
 
980
1134
 
981
1135
  def update_implementation_status(implementation: ControlImplementation, result: str) -> ControlImplementation:
@@ -1003,14 +1157,104 @@ def update_implementation_status(implementation: ControlImplementation, result:
1003
1157
  logger.info(f"Updated implementation status for {implementation.id}: {implementation.status}")
1004
1158
 
1005
1159
 
1160
+ def get_wiz_compliance_settings():
1161
+ """
1162
+ Get Wiz compliance settings for status mapping
1163
+
1164
+ :return: Compliance settings instance or None
1165
+ :rtype: Optional[ComplianceSettings]
1166
+ """
1167
+ try:
1168
+ settings = ComplianceSettings.get_by_current_tenant()
1169
+ wiz_compliance_setting = next((comp for comp in settings if comp.title == "Wiz Compliance Setting"), None)
1170
+ if not wiz_compliance_setting:
1171
+ logger.debug("No Wiz Compliance Setting found, using default implementation status mapping")
1172
+ else:
1173
+ logger.debug("Using Wiz Compliance Setting for implementation status mapping")
1174
+ return wiz_compliance_setting
1175
+ except Exception as e:
1176
+ logger.debug(f"Error getting Wiz Compliance Setting: {e}")
1177
+ return None
1178
+
1179
+
1006
1180
  def report_result_to_implementation_status(result: str) -> str:
1007
1181
  """
1008
- Convert report result to implementation status
1182
+ Convert report result to implementation status using compliance settings if available
1009
1183
 
1010
1184
  :param str result: Report result
1011
1185
  :return: Implementation status
1012
1186
  :rtype: str
1013
1187
  """
1188
+ compliance_settings = get_wiz_compliance_settings()
1189
+
1190
+ if compliance_settings:
1191
+ status = _get_status_from_compliance_settings(result, compliance_settings)
1192
+ if status:
1193
+ return status
1194
+
1195
+ # Fallback to default mapping
1196
+ return _get_default_status_mapping(result)
1197
+
1198
+
1199
+ def _get_status_from_compliance_settings(result: str, compliance_settings) -> Optional[str]:
1200
+ """
1201
+ Get implementation status from compliance settings
1202
+
1203
+ :param str result: Report result
1204
+ :param compliance_settings: Compliance settings object
1205
+ :return: Implementation status or None if not found
1206
+ :rtype: Optional[str]
1207
+ """
1208
+ try:
1209
+ status_labels = compliance_settings.get_field_labels("implementationStatus")
1210
+ result_lower = result.lower()
1211
+
1212
+ for label in status_labels:
1213
+ status = _match_result_to_label(result_lower, label)
1214
+ if status:
1215
+ return status
1216
+
1217
+ logger.debug(f"No matching compliance setting found for result: {result}")
1218
+ return None
1219
+
1220
+ except Exception as e:
1221
+ logger.debug(f"Error using compliance settings for implementation status mapping: {e}")
1222
+ return None
1223
+
1224
+
1225
+ def _match_result_to_label(result_lower: str, label: str) -> Optional[str]:
1226
+ """
1227
+ Match a result to a status label based on predefined mappings
1228
+
1229
+ :param str result_lower: Lowercase result string
1230
+ :param str label: Status label to check
1231
+ :return: Matched label or None
1232
+ :rtype: Optional[str]
1233
+ """
1234
+ label_lower = label.lower()
1235
+
1236
+ if result_lower == ComplianceCheckStatus.PASS.value.lower():
1237
+ return label if label_lower in ["implemented", "complete", "compliant"] else None
1238
+
1239
+ if result_lower == ComplianceCheckStatus.FAIL.value.lower():
1240
+ return (
1241
+ label
1242
+ if label_lower in ["inremediation", "in remediation", "remediation", "failed", "non-compliant"]
1243
+ else None
1244
+ )
1245
+
1246
+ # Not implemented or other status
1247
+ return label if label_lower in ["notimplemented", "not implemented", "pending", "planned"] else None
1248
+
1249
+
1250
+ def _get_default_status_mapping(result: str) -> str:
1251
+ """
1252
+ Get default status mapping for a result
1253
+
1254
+ :param str result: Report result
1255
+ :return: Default implementation status
1256
+ :rtype: str
1257
+ """
1014
1258
  if result == ComplianceCheckStatus.PASS.value:
1015
1259
  return ControlImplementationStatus.Implemented.value
1016
1260
  elif result == ComplianceCheckStatus.FAIL.value:
@@ -3,6 +3,7 @@
3
3
  """Wiz Variables"""
4
4
 
5
5
  from regscale.core.app.utils.variables import RsVariableType, RsVariablesMeta
6
+ from regscale.integrations.commercial.wizv2.constants import RECOMMENDED_WIZ_INVENTORY_TYPES
6
7
 
7
8
 
8
9
  class WizVariables(metaclass=RsVariablesMeta):
@@ -22,16 +23,7 @@ class WizVariables(metaclass=RsVariablesMeta):
22
23
  wizInventoryFilterBy: RsVariableType(
23
24
  str,
24
25
  '{"projectId": ["xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"], "type": ["API_GATEWAY"]}',
25
- default="""{"type":
26
- [ "API_GATEWAY", "BACKUP_SERVICE", "CDN", "CICD_SERVICE", "CLOUD_LOG_CONFIGURATION",
27
- "CLOUD_ORGANIZATION", "CONTAINER", "CONTAINER_IMAGE", "CONTAINER_REGISTRY", "CONTAINER_SERVICE",
28
- "CONTROLLER_REVISION", "DATABASE", "DATA_WORKLOAD", "DB_SERVER", "DOMAIN", "EMAIL_SERVICE", "ENCRYPTION_KEY",
29
- "FILE_SYSTEM_SERVICE", "FIREWALL", "GATEWAY", "KUBERNETES_CLUSTER", "LOAD_BALANCER",
30
- "MANAGED_CERTIFICATE", "MESSAGING_SERVICE", "NAMESPACE", "NETWORK_INTERFACE", "PRIVATE_ENDPOINT",
31
- "PRIVATE_LINK", "RAW_ACCESS_POLICY", "REGISTERED_DOMAIN", "RESOURCE_GROUP", "SECRET",
32
- "SECRET_CONTAINER", "SERVERLESS", "SERVERLESS_PACKAGE", "SERVICE_ACCOUNT", "SERVICE_CONFIGURATION",
33
- "STORAGE_ACCOUNT", "SUBNET", "SUBSCRIPTION", "VIRTUAL_DESKTOP", "VIRTUAL_MACHINE",
34
- "VIRTUAL_MACHINE_IMAGE", "VIRTUAL_NETWORK", "VOLUME", "WEB_SERVICE", "NETWORK_ADDRESS"] }""",
26
+ default="""{"type": ["%s"] }""" % '","'.join(RECOMMENDED_WIZ_INVENTORY_TYPES), # type: ignore
35
27
  ) # type: ignore
36
28
  wizAccessToken: RsVariableType(str, "", sensitive=True, required=False) # type: ignore
37
29
  wizClientId: RsVariableType(str, "", sensitive=True) # type: ignore
@@ -1656,15 +1656,21 @@ class ScannerIntegration(ABC):
1656
1656
  bulk_update=True, defaults={"otherIdentifier": self._get_other_identifier(finding, is_poam)}
1657
1657
  )
1658
1658
 
1659
- self._handle_property_creation_for_issue(issue, finding)
1659
+ self._handle_property_and_milestone_creation(issue, finding, existing_issue)
1660
1660
  return issue
1661
1661
 
1662
- def _handle_property_creation_for_issue(self, issue: regscale_models.Issue, finding: IntegrationFinding) -> None:
1662
+ def _handle_property_and_milestone_creation(
1663
+ self,
1664
+ issue: regscale_models.Issue,
1665
+ finding: IntegrationFinding,
1666
+ existing_issue: Optional[regscale_models.Issue] = None,
1667
+ ) -> None:
1663
1668
  """
1664
1669
  Handles property creation for an issue based on the finding data
1665
1670
 
1666
1671
  :param regscale_models.Issue issue: The issue to handle properties for
1667
1672
  :param IntegrationFinding finding: The finding data
1673
+ :param bool new_issue: Whether this is a new issue
1668
1674
  :rtype: None
1669
1675
  """
1670
1676
  if poc := finding.point_of_contact:
@@ -1685,6 +1691,45 @@ class ScannerIntegration(ABC):
1685
1691
  ).create_or_update()
1686
1692
  logger.debug("Added CWE property %s to issue %s", finding.plugin_id, issue.id)
1687
1693
 
1694
+ if ScannerVariables.useMilestones:
1695
+ if (
1696
+ existing_issue
1697
+ and existing_issue.status == regscale_models.IssueStatus.Closed
1698
+ and issue.status == regscale_models.IssueStatus.Open
1699
+ ):
1700
+ regscale_models.Milestone(
1701
+ title=f"Issue reopened from {self.title} scan",
1702
+ milestoneDate=get_current_datetime(),
1703
+ responsiblePersonId=self.assessor_id,
1704
+ parentID=issue.id,
1705
+ parentModule="issues",
1706
+ ).create_or_update()
1707
+ logger.debug("Added milestone for issue %s from finding %s", issue.id, finding.external_id)
1708
+ elif (
1709
+ existing_issue
1710
+ and existing_issue.status == regscale_models.IssueStatus.Open
1711
+ and issue.status == regscale_models.IssueStatus.Closed
1712
+ ):
1713
+ regscale_models.Milestone(
1714
+ title=f"Issue closed from {self.title} scan",
1715
+ milestoneDate=issue.dateCompleted,
1716
+ responsiblePersonId=self.assessor_id,
1717
+ parentID=issue.id,
1718
+ parentModule="issues",
1719
+ ).create_or_update()
1720
+ logger.debug("Added milestone for issue %s from finding %s", issue.id, finding.external_id)
1721
+ elif not existing_issue:
1722
+ regscale_models.Milestone(
1723
+ title=f"Issue created from {self.title} scan",
1724
+ milestoneDate=self.scan_date,
1725
+ responsiblePersonId=self.assessor_id,
1726
+ parentID=issue.id,
1727
+ parentModule="issues",
1728
+ ).create_or_update()
1729
+ logger.debug("Created milestone for issue %s from finding %s", issue.id, finding.external_id)
1730
+ else:
1731
+ logger.debug("No milestone created for issue %s from finding %s", issue.id, finding.external_id)
1732
+
1688
1733
  @staticmethod
1689
1734
  def get_consolidated_asset_identifier(
1690
1735
  finding: IntegrationFinding,
@@ -2523,6 +2568,17 @@ class ScannerIntegration(ABC):
2523
2568
  issue.dateLastUpdated = get_current_datetime()
2524
2569
  issue.save()
2525
2570
 
2571
+ if ScannerVariables.useMilestones:
2572
+ regscale_models.Milestone(
2573
+ title=f"Issue closed from {self.title} scan",
2574
+ milestoneDate=issue.dateCompleted,
2575
+ responsiblePersonId=self.assessor_id,
2576
+ completed=True,
2577
+ parentID=issue.id,
2578
+ parentModule="issues",
2579
+ ).create_or_update()
2580
+ logger.debug("Created milestone for issue %s from %s tool", issue.id, self.title)
2581
+
2526
2582
  with count_lock:
2527
2583
  self.closed_count += 1
2528
2584
  if issue.controlImplementationIds:
@@ -26,3 +26,4 @@ class ScannerVariables(metaclass=RsVariablesMeta):
26
26
  maxRetries: RsVariableType(int, "3", default=3, required=False) # type: ignore
27
27
  timeout: RsVariableType(int, "60", default=60, required=False) # type: ignore
28
28
  complianceCreation: RsVariableType(str, "Assessment|Issue|POAM", default="Assessment", required=False) # type: ignore # noqa: F722,F821
29
+ useMilestones: RsVariableType(bool, "true|false", default=False, required=False) # type: ignore # noqa: F722,F722,F821