regscale-cli 6.25.0.1__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 (84) 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/synqly/assets.py +17 -0
  11. regscale/integrations/commercial/wizv2/click.py +26 -26
  12. regscale/integrations/commercial/wizv2/compliance_report.py +152 -157
  13. regscale/integrations/commercial/wizv2/constants.py +20 -71
  14. regscale/integrations/commercial/wizv2/scanner.py +3 -3
  15. regscale/integrations/compliance_integration.py +67 -2
  16. regscale/integrations/control_matcher.py +358 -0
  17. regscale/integrations/due_date_handler.py +118 -6
  18. regscale/integrations/milestone_manager.py +291 -0
  19. regscale/integrations/public/__init__.py +1 -0
  20. regscale/integrations/public/cci_importer.py +37 -38
  21. regscale/integrations/public/fedramp/click.py +60 -2
  22. regscale/integrations/public/fedramp/poam_export_v5.py +888 -0
  23. regscale/integrations/scanner_integration.py +199 -130
  24. regscale/models/integration_models/cisa_kev_data.json +199 -4
  25. regscale/models/integration_models/nexpose.py +36 -10
  26. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  27. regscale/models/locking.py +12 -8
  28. regscale/models/platform.py +1 -2
  29. regscale/models/regscale_models/control_implementation.py +46 -21
  30. regscale/models/regscale_models/issue.py +256 -94
  31. regscale/models/regscale_models/milestone.py +1 -1
  32. regscale/models/regscale_models/regscale_model.py +6 -1
  33. regscale/templates/__init__.py +0 -0
  34. regscale/utils/threading/threadhandler.py +20 -15
  35. {regscale_cli-6.25.0.1.dist-info → regscale_cli-6.26.0.0.dist-info}/METADATA +1 -1
  36. {regscale_cli-6.25.0.1.dist-info → regscale_cli-6.26.0.0.dist-info}/RECORD +84 -37
  37. tests/regscale/integrations/commercial/__init__.py +0 -0
  38. tests/regscale/integrations/commercial/conftest.py +28 -0
  39. tests/regscale/integrations/commercial/microsoft_defender/__init__.py +1 -0
  40. tests/regscale/integrations/commercial/microsoft_defender/test_defender.py +1517 -0
  41. tests/regscale/integrations/commercial/microsoft_defender/test_defender_api.py +1748 -0
  42. tests/regscale/integrations/commercial/microsoft_defender/test_defender_constants.py +327 -0
  43. tests/regscale/integrations/commercial/microsoft_defender/test_defender_scanner.py +487 -0
  44. tests/regscale/integrations/commercial/test_aws.py +3731 -0
  45. tests/regscale/integrations/commercial/test_burp.py +48 -0
  46. tests/regscale/integrations/commercial/test_crowdstrike.py +49 -0
  47. tests/regscale/integrations/commercial/test_dependabot.py +341 -0
  48. tests/regscale/integrations/commercial/test_gcp.py +1543 -0
  49. tests/regscale/integrations/commercial/test_gitlab.py +549 -0
  50. tests/regscale/integrations/commercial/test_ip_mac_address_length.py +84 -0
  51. tests/regscale/integrations/commercial/test_jira.py +1814 -0
  52. tests/regscale/integrations/commercial/test_npm_audit.py +42 -0
  53. tests/regscale/integrations/commercial/test_okta.py +1228 -0
  54. tests/regscale/integrations/commercial/test_sarif_converter.py +251 -0
  55. tests/regscale/integrations/commercial/test_sicura.py +350 -0
  56. tests/regscale/integrations/commercial/test_snow.py +423 -0
  57. tests/regscale/integrations/commercial/test_sonarcloud.py +394 -0
  58. tests/regscale/integrations/commercial/test_sqlserver.py +186 -0
  59. tests/regscale/integrations/commercial/test_stig.py +33 -0
  60. tests/regscale/integrations/commercial/test_stig_mapper.py +153 -0
  61. tests/regscale/integrations/commercial/test_stigv2.py +406 -0
  62. tests/regscale/integrations/commercial/test_wiz.py +1469 -0
  63. tests/regscale/integrations/commercial/test_wiz_inventory.py +256 -0
  64. tests/regscale/integrations/commercial/wizv2/__init__.py +339 -0
  65. tests/regscale/integrations/commercial/wizv2/test_compliance_report_normalization.py +138 -0
  66. tests/regscale/integrations/commercial/wizv2/test_issue.py +343 -0
  67. tests/regscale/integrations/commercial/wizv2/test_wiz_click_client_id.py +165 -0
  68. tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_report.py +1351 -0
  69. tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_unit.py +341 -0
  70. tests/regscale/integrations/commercial/wizv2/test_wiz_control_normalization.py +138 -0
  71. tests/regscale/integrations/commercial/wizv2/test_wiz_policy_compliance.py +750 -0
  72. tests/regscale/integrations/commercial/wizv2/test_wiz_status_mapping.py +149 -0
  73. tests/regscale/integrations/commercial/wizv2/test_wizv2.py +264 -0
  74. tests/regscale/integrations/commercial/wizv2/test_wizv2_utils.py +624 -0
  75. tests/regscale/integrations/public/fedramp/__init__.py +1 -0
  76. tests/regscale/integrations/public/fedramp/test_poam_export_v5.py +1293 -0
  77. tests/regscale/integrations/test_control_matcher.py +1314 -0
  78. tests/regscale/integrations/test_control_matching.py +155 -0
  79. tests/regscale/integrations/test_milestone_manager.py +408 -0
  80. tests/regscale/models/test_issue.py +378 -1
  81. {regscale_cli-6.25.0.1.dist-info → regscale_cli-6.26.0.0.dist-info}/LICENSE +0 -0
  82. {regscale_cli-6.25.0.1.dist-info → regscale_cli-6.26.0.0.dist-info}/WHEEL +0 -0
  83. {regscale_cli-6.25.0.1.dist-info → regscale_cli-6.26.0.0.dist-info}/entry_points.txt +0 -0
  84. {regscale_cli-6.25.0.1.dist-info → regscale_cli-6.26.0.0.dist-info}/top_level.txt +0 -0
@@ -27,6 +27,7 @@ from regscale.integrations.commercial.durosuite.process_devices import scan_duro
27
27
  from regscale.integrations.commercial.durosuite.variables import DuroSuiteVariables
28
28
  from regscale.integrations.commercial.stig_mapper_integration.mapping_engine import StigMappingEngine as STIGMapper
29
29
  from regscale.integrations.due_date_handler import DueDateHandler
30
+ from regscale.integrations.milestone_manager import MilestoneManager
30
31
  from regscale.integrations.public.cisa import pull_cisa_kev
31
32
  from regscale.integrations.variables import ScannerVariables
32
33
  from regscale.models import DateTimeEncoder, OpenIssueDict, Property, regscale_models
@@ -48,6 +49,31 @@ def get_thread_workers_max() -> int:
48
49
  return ScannerVariables.threadMaxWorkers
49
50
 
50
51
 
52
+ def _create_config_override(
53
+ config: Optional[Dict[str, Dict]],
54
+ integration_name: str,
55
+ critical: Optional[int],
56
+ high: Optional[int],
57
+ moderate: Optional[int],
58
+ low: Optional[int],
59
+ ) -> Dict[str, Dict]:
60
+ """Create a config override for legacy parameter support."""
61
+ override_config = config.copy() if config else {}
62
+ if "issues" not in override_config:
63
+ override_config["issues"] = {}
64
+ if integration_name not in override_config["issues"]:
65
+ override_config["issues"][integration_name] = {}
66
+
67
+ integration_config = override_config["issues"][integration_name]
68
+ severity_params = {"critical": critical, "high": high, "moderate": moderate, "low": low}
69
+
70
+ for param_name, param_value in severity_params.items():
71
+ if param_value is not None:
72
+ integration_config[param_name] = param_value
73
+
74
+ return override_config
75
+
76
+
51
77
  def issue_due_date(
52
78
  severity: regscale_models.IssueSeverity,
53
79
  created_date: str,
@@ -61,6 +87,9 @@ def issue_due_date(
61
87
  """
62
88
  Calculate the due date for an issue based on its severity and creation date.
63
89
 
90
+ DEPRECATED: This function is kept for backward compatibility. New code should use DueDateHandler directly.
91
+ This function now uses DueDateHandler internally to ensure consistent behavior and proper validation.
92
+
64
93
  :param regscale_models.IssueSeverity severity: The severity of the issue.
65
94
  :param str created_date: The creation date of the issue.
66
95
  :param Optional[int] critical: Days until due for high severity issues.
@@ -72,40 +101,19 @@ def issue_due_date(
72
101
  :return: The due date for the issue.
73
102
  :rtype: str
74
103
  """
75
- if critical is None:
76
- critical = ScannerVariables.issueDueDates.get("critical", 30)
77
- if high is None:
78
- high = ScannerVariables.issueDueDates.get("high", 60)
79
- if moderate is None:
80
- moderate = ScannerVariables.issueDueDates.get("moderate", 120)
81
- if low is None:
82
- low = ScannerVariables.issueDueDates.get("low", 364)
83
-
84
- if config is None:
85
- config = {}
86
-
87
- due_date_map = {
88
- regscale_models.IssueSeverity.Critical: critical,
89
- regscale_models.IssueSeverity.High: high,
90
- regscale_models.IssueSeverity.Moderate: moderate,
91
- regscale_models.IssueSeverity.Low: low,
92
- }
93
-
94
- if title and config:
95
- # if title in a config key, use that key
96
- issues_dict = config.get("issues", {})
97
- matching_key = next((key.lower() for key in issues_dict if title.lower() in key.lower()), None)
98
- if matching_key:
99
- title_config = issues_dict.get(matching_key, {})
100
- due_date_map = {
101
- regscale_models.IssueSeverity.Critical: title_config.get("critical", critical),
102
- regscale_models.IssueSeverity.High: title_config.get("high", high),
103
- regscale_models.IssueSeverity.Moderate: title_config.get("moderate", moderate),
104
- regscale_models.IssueSeverity.Low: title_config.get("low", low),
105
- }
106
-
107
- days = due_date_map.get(severity, low)
108
- return date_str(get_day_increment(start=created_date, days=days))
104
+ integration_name = title or "default"
105
+
106
+ # Check if individual parameters need config override
107
+ if any(param is not None for param in [critical, high, moderate, low]):
108
+ config = _create_config_override(config, integration_name, critical, high, moderate, low)
109
+
110
+ due_date_handler = DueDateHandler(integration_name, config=config)
111
+ return due_date_handler.calculate_due_date(
112
+ severity=severity,
113
+ created_date=created_date,
114
+ cve=None, # Legacy function doesn't have CVE parameter
115
+ title=title,
116
+ )
109
117
 
110
118
 
111
119
  class ManagedDefaultDict(Generic[K, V]):
@@ -665,6 +673,9 @@ class ScannerIntegration(ABC):
665
673
  # Initialize due date handler for this integration
666
674
  self.due_date_handler = DueDateHandler(self.title, config=self.app.config)
667
675
 
676
+ # Initialize milestone manager for this integration
677
+ self.milestone_manager = None # Lazy initialization after scan_date is set
678
+
668
679
  if self.is_component:
669
680
  self.component = regscale_models.Component.get_object(self.plan_id)
670
681
  self.parent_module: str = regscale_models.Component.get_module_string()
@@ -730,6 +741,21 @@ class ScannerIntegration(ABC):
730
741
  cls._lock_registry[key] = lock
731
742
  return lock
732
743
 
744
+ def get_milestone_manager(self) -> MilestoneManager:
745
+ """
746
+ Get or initialize the milestone manager.
747
+
748
+ :return: MilestoneManager instance
749
+ :rtype: MilestoneManager
750
+ """
751
+ if self.milestone_manager is None:
752
+ self.milestone_manager = MilestoneManager(
753
+ integration_title=self.title,
754
+ assessor_id=self.assessor_id,
755
+ scan_date=self.scan_date or get_current_datetime(),
756
+ )
757
+ return self.milestone_manager
758
+
733
759
  @staticmethod
734
760
  def load_stig_mapper() -> Optional[STIGMapper]:
735
761
  """
@@ -990,15 +1016,18 @@ class ScannerIntegration(ABC):
990
1016
  return res[:450]
991
1017
  return prefix[:450]
992
1018
 
993
- def get_or_create_assessment(self, control_implementation_id: int) -> regscale_models.Assessment:
1019
+ def get_or_create_assessment(
1020
+ self, control_implementation_id: int, status: Optional[regscale_models.AssessmentResultsStatus] = None
1021
+ ) -> regscale_models.Assessment:
994
1022
  """
995
- Gets or creates a RegScale assessment
1023
+ Gets or creates a RegScale assessment.
996
1024
 
997
1025
  :param int control_implementation_id: The ID of the control implementation
1026
+ :param Optional[regscale_models.AssessmentResultsStatus] status: Optional status override (used by cci_assessment)
998
1027
  :return: The assessment
999
1028
  :rtype: regscale_models.Assessment
1000
1029
  """
1001
- logger.info("Getting or create assessment for control implementation %d", control_implementation_id)
1030
+ logger.debug("Getting or create assessment for control implementation %d", control_implementation_id)
1002
1031
  assessment: Optional[regscale_models.Assessment] = self.assessment_map.get(control_implementation_id)
1003
1032
  if assessment:
1004
1033
  logger.debug(
@@ -1010,7 +1039,7 @@ class ScannerIntegration(ABC):
1010
1039
  plannedStart=get_current_datetime(),
1011
1040
  plannedFinish=get_current_datetime(),
1012
1041
  status=regscale_models.AssessmentStatus.COMPLETE.value,
1013
- assessmentResult=regscale_models.AssessmentResultsStatus.FAIL.value,
1042
+ assessmentResult=status.value if status else regscale_models.AssessmentResultsStatus.FAIL.value,
1014
1043
  actualFinish=get_current_datetime(),
1015
1044
  leadAssessorId=self.assessor_id,
1016
1045
  parentId=control_implementation_id,
@@ -2106,7 +2135,9 @@ class ScannerIntegration(ABC):
2106
2135
 
2107
2136
  def _set_issue_due_date(self, issue: regscale_models.Issue, finding: IntegrationFinding) -> None:
2108
2137
  """Set the due date for the issue using DueDateHandler."""
2138
+ # Always calculate or validate due date to ensure it's not in the past
2109
2139
  if not finding.due_date:
2140
+ # No due date set, calculate new one
2110
2141
  try:
2111
2142
  base_created = finding.date_created or issue.dateCreated
2112
2143
  finding.due_date = self.due_date_handler.calculate_due_date(
@@ -2125,6 +2156,12 @@ class ScannerIntegration(ABC):
2125
2156
  cve=finding.cve,
2126
2157
  title=finding.title or self.title,
2127
2158
  )
2159
+ else:
2160
+ # Due date already exists, but validate it's not in the past (if noPastDueDates is enabled)
2161
+ finding.due_date = self.due_date_handler._ensure_future_due_date(
2162
+ finding.due_date, self.due_date_handler.integration_timelines.get(finding.severity, 60)
2163
+ )
2164
+
2128
2165
  issue.dueDate = finding.due_date
2129
2166
 
2130
2167
  def _set_additional_issue_fields(
@@ -2269,87 +2306,26 @@ class ScannerIntegration(ABC):
2269
2306
  """
2270
2307
  Create milestones for an issue based on status transitions.
2271
2308
 
2309
+ Delegates to MilestoneManager for cleaner separation of concerns.
2310
+ Also ensures existing issues have creation milestones (backfills if missing).
2311
+
2272
2312
  :param regscale_models.Issue issue: The issue to create milestones for
2273
2313
  :param IntegrationFinding finding: The finding data
2274
2314
  :param Optional[regscale_models.Issue] existing_issue: Existing issue for comparison
2275
2315
  """
2276
- if not (ScannerVariables.useMilestones and issue.id):
2277
- return
2278
-
2279
- if self._should_create_reopened_milestone(existing_issue, issue):
2280
- self._create_milestone_safe(
2281
- issue, finding, "Issue reopened from", get_current_datetime(), "reopened milestone"
2282
- )
2283
- elif self._should_create_closed_milestone(existing_issue, issue):
2284
- self._create_milestone_safe(issue, finding, "Issue closed from", issue.dateCompleted, "closed milestone")
2285
- elif not existing_issue:
2286
- self._create_milestone_safe(issue, finding, "Issue created from", self.scan_date, "new issue milestone")
2287
- else:
2288
- logger.debug("No milestone created for issue %s from finding %s", issue.id, finding.external_id)
2316
+ milestone_manager = self.get_milestone_manager()
2289
2317
 
2290
- def _should_create_reopened_milestone(
2291
- self, existing_issue: Optional[regscale_models.Issue], issue: regscale_models.Issue
2292
- ) -> bool:
2293
- """
2294
- Check if a reopened milestone should be created.
2295
-
2296
- :param Optional[regscale_models.Issue] existing_issue: The existing issue
2297
- :param regscale_models.Issue issue: The current issue
2298
- :return: True if reopened milestone should be created
2299
- :rtype: bool
2300
- """
2301
- return (
2302
- existing_issue
2303
- and existing_issue.status == regscale_models.IssueStatus.Closed
2304
- and issue.status == regscale_models.IssueStatus.Open
2305
- )
2306
-
2307
- def _should_create_closed_milestone(
2308
- self, existing_issue: Optional[regscale_models.Issue], issue: regscale_models.Issue
2309
- ) -> bool:
2310
- """
2311
- Check if a closed milestone should be created.
2318
+ # For existing issues, ensure they have a creation milestone (backfill if missing)
2319
+ if existing_issue:
2320
+ milestone_manager.ensure_creation_milestone_exists(issue=issue, finding=finding)
2312
2321
 
2313
- :param Optional[regscale_models.Issue] existing_issue: The existing issue
2314
- :param regscale_models.Issue issue: The current issue
2315
- :return: True if closed milestone should be created
2316
- :rtype: bool
2317
- """
2318
- return (
2319
- existing_issue
2320
- and existing_issue.status == regscale_models.IssueStatus.Open
2321
- and issue.status == regscale_models.IssueStatus.Closed
2322
+ # Handle status transition milestones
2323
+ milestone_manager.create_milestones_for_issue(
2324
+ issue=issue,
2325
+ finding=finding,
2326
+ existing_issue=existing_issue,
2322
2327
  )
2323
2328
 
2324
- def _create_milestone_safe(
2325
- self,
2326
- issue: regscale_models.Issue,
2327
- finding: IntegrationFinding,
2328
- title_prefix: str,
2329
- milestone_date: str,
2330
- milestone_type: str,
2331
- ) -> None:
2332
- """
2333
- Safely create a milestone with error handling.
2334
-
2335
- :param regscale_models.Issue issue: The issue to create milestone for
2336
- :param IntegrationFinding finding: The finding data
2337
- :param str title_prefix: Prefix for milestone title
2338
- :param str milestone_date: Date for the milestone
2339
- :param str milestone_type: Description for logging purposes
2340
- """
2341
- try:
2342
- regscale_models.Milestone(
2343
- title=f"{title_prefix} {self.title} scan",
2344
- milestoneDate=milestone_date,
2345
- responsiblePersonId=self.assessor_id,
2346
- parentID=issue.id,
2347
- parentModule="issues",
2348
- ).create_or_update()
2349
- logger.debug("Added milestone for issue %s from finding %s", issue.id, finding.external_id)
2350
- except Exception as e:
2351
- logger.warning("Failed to create %s: %s", milestone_type, str(e))
2352
-
2353
2329
  @staticmethod
2354
2330
  def extra_data_to_properties(finding: IntegrationFinding, issue_id: int) -> None:
2355
2331
  """
@@ -2515,6 +2491,8 @@ class ScannerIntegration(ABC):
2515
2491
  if found_issue.controlImplementationIds:
2516
2492
  for control_id in found_issue.controlImplementationIds:
2517
2493
  self.update_control_implementation_status_after_close(control_id)
2494
+ # Update assessment status to reflect the control implementation status
2495
+ self.update_assessment_status_from_control_implementation(control_id)
2518
2496
 
2519
2497
  def handle_failing_checklist(
2520
2498
  self,
@@ -2539,11 +2517,13 @@ class ScannerIntegration(ABC):
2539
2517
  if failing_objective.name.lower().startswith("cci-"):
2540
2518
  implementation_id = self.get_control_implementation_id_for_cci(failing_objective.name)
2541
2519
  else:
2542
- control_label = objective_to_control_dot(failing_objective.name)
2543
- if control_label not in self.control_implementation_id_map:
2544
- logger.warning("Control %s not found for %s", control_label, control_label)
2545
- continue
2546
- implementation_id = self.control_implementation_id_map[control_label]
2520
+ implementation_id = self._fallback_implementation_id(failing_objective)
2521
+
2522
+ if not implementation_id or implementation_id is None:
2523
+ logger.warning(
2524
+ "Could not map objective to a Control Implementation for objective #%i.", failing_objective.id
2525
+ )
2526
+ continue
2547
2527
 
2548
2528
  failing_option = regscale_models.ImplementationOption(
2549
2529
  name="Failed STIG",
@@ -2565,13 +2545,36 @@ class ScannerIntegration(ABC):
2565
2545
  ).create_or_update()
2566
2546
 
2567
2547
  # Create assessment and control test result
2568
- assessment = self.get_or_create_assessment(implementation_id)
2548
+ assessment = self.get_or_create_assessment(
2549
+ implementation_id, status=regscale_models.AssessmentResultsStatus.FAIL
2550
+ )
2569
2551
  if implementation_id:
2570
2552
  control_test = self.create_or_get_control_test(finding, implementation_id)
2571
2553
  self.create_control_test_result(
2572
2554
  finding, control_test, assessment, regscale_models.ControlTestResultStatus.FAIL
2573
2555
  )
2574
2556
 
2557
+ def _fallback_implementation_id(self, objective: regscale_models.ControlObjective) -> Optional[int]:
2558
+ """
2559
+ Fallback method to get control implementation ID from objective name if CCI mapping fails.
2560
+
2561
+ :param regscale_models.ControlObjective objective: The control objective
2562
+ :return: The control implementation ID if found, None otherwise
2563
+ :rtype: Optional[int]
2564
+ """
2565
+ control_label = objective_to_control_dot(objective.name)
2566
+ if implementation_id := self.control_implementation_id_map.get(control_label):
2567
+ return implementation_id
2568
+
2569
+ if control_id := self.control_id_to_implementation_map.get(objective.securityControlId):
2570
+ if control_label := self.control_map.get(control_id):
2571
+ implementation_id = self.control_implementation_id_map.get(control_label)
2572
+ if not implementation_id:
2573
+ print("No dice.")
2574
+ return implementation_id
2575
+ logger.debug("Could not find fallback implementation ID for objective #%i", objective.id)
2576
+ return None
2577
+
2575
2578
  def handle_passing_checklist(
2576
2579
  self,
2577
2580
  finding: IntegrationFinding,
@@ -2595,15 +2598,12 @@ class ScannerIntegration(ABC):
2595
2598
  if passing_objective.name.lower().startswith("cci-"):
2596
2599
  implementation_id = self.get_control_implementation_id_for_cci(passing_objective.name)
2597
2600
  else:
2598
- control_label = objective_to_control_dot(passing_objective.name)
2599
- if control_label not in self.control_implementation_id_map:
2600
- logger.warning("Control %s not found for %s", control_label, control_label)
2601
- continue
2602
- implementation_id = self.control_implementation_id_map[control_label]
2603
-
2604
- # Skip if we couldn't determine the implementation ID
2605
- if implementation_id is None:
2606
- logger.warning("Could not determine implementation ID for objective %s", passing_objective.name)
2601
+ implementation_id = self._fallback_implementation_id(passing_objective)
2602
+
2603
+ if not implementation_id or implementation_id is None:
2604
+ logger.warning(
2605
+ "Could not map objective to a Control Implementation for objective #%i.", passing_objective.id
2606
+ )
2607
2607
  continue
2608
2608
 
2609
2609
  passing_option = regscale_models.ImplementationOption(
@@ -2626,7 +2626,9 @@ class ScannerIntegration(ABC):
2626
2626
  ).create_or_update()
2627
2627
 
2628
2628
  # Create assessment and control test result
2629
- assessment = self.get_or_create_assessment(implementation_id)
2629
+ assessment = self.get_or_create_assessment(
2630
+ implementation_id, status=regscale_models.AssessmentResultsStatus.PASS
2631
+ )
2630
2632
  control_test = self.create_or_get_control_test(finding, implementation_id)
2631
2633
  self.create_control_test_result(
2632
2634
  finding, control_test, assessment, regscale_models.ControlTestResultStatus.PASS
@@ -2691,7 +2693,11 @@ class ScannerIntegration(ABC):
2691
2693
  logger.error("2. Asset not found for identifier %s", finding.asset_identifier)
2692
2694
  return 0
2693
2695
 
2694
- tool = regscale_models.ChecklistTool.STIGs
2696
+ tool = (
2697
+ regscale_models.ChecklistTool.CISBenchmarks
2698
+ if "simp.cis" in str(finding.vulnerability_number).lower()
2699
+ else regscale_models.ChecklistTool.STIGs
2700
+ )
2695
2701
  if finding.vulnerability_type == "Vulnerability Scan":
2696
2702
  tool = regscale_models.ChecklistTool.VulnerabilityScanner
2697
2703
 
@@ -3366,6 +3372,8 @@ class ScannerIntegration(ABC):
3366
3372
 
3367
3373
  for control_id in affected_control_ids:
3368
3374
  self.update_control_implementation_status_after_close(control_id)
3375
+ # Update assessment status to reflect the control implementation status
3376
+ self.update_assessment_status_from_control_implementation(control_id)
3369
3377
 
3370
3378
  (
3371
3379
  logger.info("Closed %d outdated issues.", closed_count)
@@ -3468,9 +3476,70 @@ class ScannerIntegration(ABC):
3468
3476
  if control_implementation.status != new_status:
3469
3477
  control_implementation.status = new_status
3470
3478
  self.control_implementation_map[control_id] = control_implementation.save()
3471
- logger.info("Updated control implementation %d status to %s", control_id, new_status)
3479
+ logger.debug("Updated control implementation %d status to %s", control_id, new_status)
3480
+
3481
+ def update_assessment_status_from_control_implementation(self, control_implementation_id: int) -> None:
3482
+ """
3483
+ Updates the assessment status based on the control implementation status.
3484
+ Treats the ControlImplementation status as the source of truth.
3485
+
3486
+ Sets assessment to PASS if ControlImplementation status is FULLY_IMPLEMENTED,
3487
+ otherwise sets it to FAIL.
3472
3488
 
3473
- def is_issue_protected_from_auto_close(self, issue: regscale_models.Issue) -> bool:
3489
+ This method should be called after update_control_implementation_status_after_close
3490
+ to ensure assessments reflect the final control implementation state.
3491
+
3492
+ :param int control_implementation_id: The ID of the control implementation
3493
+ :rtype: None
3494
+ """
3495
+ # Get the cached assessment for this control implementation
3496
+ assessment = self.assessment_map.get(control_implementation_id)
3497
+
3498
+ if not assessment:
3499
+ logger.debug(
3500
+ "No assessment found in cache for control implementation %d, skipping assessment update",
3501
+ control_implementation_id,
3502
+ )
3503
+ return
3504
+
3505
+ # Get the control implementation to check its status
3506
+ control_implementation = self.control_implementation_map.get(
3507
+ control_implementation_id
3508
+ ) or regscale_models.ControlImplementation.get_object(object_id=control_implementation_id)
3509
+
3510
+ if not control_implementation:
3511
+ logger.warning("Control implementation %d not found, cannot update assessment", control_implementation_id)
3512
+ return
3513
+
3514
+ # Determine assessment result based on control implementation status
3515
+ # Treat ControlImplementation status as the source of truth
3516
+ new_assessment_result = (
3517
+ regscale_models.AssessmentResultsStatus.PASS
3518
+ if control_implementation.status == regscale_models.ImplementationStatus.FULLY_IMPLEMENTED.value
3519
+ else regscale_models.AssessmentResultsStatus.FAIL
3520
+ )
3521
+
3522
+ # Only update if the status has changed
3523
+ if assessment.assessmentResult != new_assessment_result.value:
3524
+ assessment.assessmentResult = new_assessment_result.value
3525
+ assessment.save()
3526
+ logger.debug(
3527
+ "Updated assessment %d for control implementation %d: assessmentResult=%s (based on control status: %s)",
3528
+ assessment.id,
3529
+ control_implementation_id,
3530
+ new_assessment_result.value,
3531
+ control_implementation.status,
3532
+ )
3533
+ else:
3534
+ logger.debug(
3535
+ "Assessment %d already has correct status %s for control implementation %d",
3536
+ assessment.id,
3537
+ assessment.assessmentResult,
3538
+ control_implementation_id,
3539
+ )
3540
+
3541
+ @staticmethod
3542
+ def is_issue_protected_from_auto_close(issue: regscale_models.Issue) -> bool:
3474
3543
  """
3475
3544
  Check if an issue is protected from automatic closure.
3476
3545