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

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

Potentially problematic release.


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

Files changed (146) hide show
  1. regscale/_version.py +1 -1
  2. regscale/airflow/hierarchy.py +2 -2
  3. regscale/core/app/application.py +19 -4
  4. regscale/core/app/internal/evidence.py +419 -2
  5. regscale/core/app/internal/login.py +0 -1
  6. regscale/core/app/utils/catalog_utils/common.py +1 -1
  7. regscale/dev/code_gen.py +24 -20
  8. regscale/integrations/commercial/jira.py +367 -126
  9. regscale/integrations/commercial/qualys/__init__.py +7 -8
  10. regscale/integrations/commercial/qualys/scanner.py +8 -3
  11. regscale/integrations/commercial/sicura/api.py +14 -13
  12. regscale/integrations/commercial/sicura/commands.py +8 -2
  13. regscale/integrations/commercial/sicura/scanner.py +49 -39
  14. regscale/integrations/commercial/stigv2/ckl_parser.py +5 -5
  15. regscale/integrations/commercial/synqly/assets.py +17 -0
  16. regscale/integrations/commercial/synqly/vulnerabilities.py +45 -28
  17. regscale/integrations/commercial/tenablev2/cis_parsers.py +453 -0
  18. regscale/integrations/commercial/tenablev2/cis_scanner.py +447 -0
  19. regscale/integrations/commercial/tenablev2/commands.py +142 -1
  20. regscale/integrations/commercial/tenablev2/scanner.py +0 -1
  21. regscale/integrations/commercial/tenablev2/stig_parsers.py +113 -57
  22. regscale/integrations/commercial/wizv2/WizDataMixin.py +1 -1
  23. regscale/integrations/commercial/wizv2/click.py +64 -79
  24. regscale/integrations/commercial/wizv2/compliance/__init__.py +15 -0
  25. regscale/integrations/commercial/wizv2/{policy_compliance_helpers.py → compliance/helpers.py} +78 -60
  26. regscale/integrations/commercial/wizv2/compliance_report.py +161 -165
  27. regscale/integrations/commercial/wizv2/core/__init__.py +133 -0
  28. regscale/integrations/commercial/wizv2/{async_client.py → core/client.py} +3 -3
  29. regscale/integrations/commercial/wizv2/{constants.py → core/constants.py} +1 -17
  30. regscale/integrations/commercial/wizv2/core/file_operations.py +237 -0
  31. regscale/integrations/commercial/wizv2/fetchers/__init__.py +11 -0
  32. regscale/integrations/commercial/wizv2/{data_fetcher.py → fetchers/policy_assessment.py} +5 -9
  33. regscale/integrations/commercial/wizv2/issue.py +1 -1
  34. regscale/integrations/commercial/wizv2/models/__init__.py +0 -0
  35. regscale/integrations/commercial/wizv2/parsers/__init__.py +34 -0
  36. regscale/integrations/commercial/wizv2/{parsers.py → parsers/main.py} +1 -1
  37. regscale/integrations/commercial/wizv2/processors/__init__.py +11 -0
  38. regscale/integrations/commercial/wizv2/{finding_processor.py → processors/finding.py} +1 -1
  39. regscale/integrations/commercial/wizv2/reports.py +1 -1
  40. regscale/integrations/commercial/wizv2/sbom.py +1 -1
  41. regscale/integrations/commercial/wizv2/scanner.py +39 -99
  42. regscale/integrations/commercial/wizv2/utils/__init__.py +48 -0
  43. regscale/integrations/commercial/wizv2/{utils.py → utils/main.py} +116 -61
  44. regscale/integrations/commercial/wizv2/variables.py +89 -3
  45. regscale/integrations/compliance_integration.py +60 -41
  46. regscale/integrations/control_matcher.py +377 -0
  47. regscale/integrations/due_date_handler.py +14 -8
  48. regscale/integrations/milestone_manager.py +291 -0
  49. regscale/integrations/public/__init__.py +1 -0
  50. regscale/integrations/public/cci_importer.py +37 -38
  51. regscale/integrations/public/fedramp/click.py +60 -2
  52. regscale/integrations/public/fedramp/docx_parser.py +10 -1
  53. regscale/integrations/public/fedramp/fedramp_cis_crm.py +393 -340
  54. regscale/integrations/public/fedramp/fedramp_five.py +1 -1
  55. regscale/integrations/public/fedramp/poam_export_v5.py +888 -0
  56. regscale/integrations/scanner_integration.py +277 -153
  57. regscale/models/integration_models/cisa_kev_data.json +282 -9
  58. regscale/models/integration_models/nexpose.py +36 -10
  59. regscale/models/integration_models/qualys.py +3 -4
  60. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  61. regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +24 -7
  62. regscale/models/integration_models/synqly_models/synqly_model.py +8 -1
  63. regscale/models/locking.py +12 -8
  64. regscale/models/platform.py +1 -2
  65. regscale/models/regscale_models/control_implementation.py +47 -22
  66. regscale/models/regscale_models/issue.py +256 -95
  67. regscale/models/regscale_models/milestone.py +1 -1
  68. regscale/models/regscale_models/regscale_model.py +6 -1
  69. regscale/templates/__init__.py +0 -0
  70. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/METADATA +1 -17
  71. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/RECORD +145 -65
  72. tests/regscale/integrations/commercial/__init__.py +0 -0
  73. tests/regscale/integrations/commercial/conftest.py +28 -0
  74. tests/regscale/integrations/commercial/microsoft_defender/__init__.py +1 -0
  75. tests/regscale/integrations/commercial/microsoft_defender/test_defender.py +1517 -0
  76. tests/regscale/integrations/commercial/microsoft_defender/test_defender_api.py +1748 -0
  77. tests/regscale/integrations/commercial/microsoft_defender/test_defender_constants.py +327 -0
  78. tests/regscale/integrations/commercial/microsoft_defender/test_defender_scanner.py +487 -0
  79. tests/regscale/integrations/commercial/test_aws.py +3731 -0
  80. tests/regscale/integrations/commercial/test_burp.py +48 -0
  81. tests/regscale/integrations/commercial/test_crowdstrike.py +49 -0
  82. tests/regscale/integrations/commercial/test_dependabot.py +341 -0
  83. tests/regscale/integrations/commercial/test_gcp.py +1543 -0
  84. tests/regscale/integrations/commercial/test_gitlab.py +549 -0
  85. tests/regscale/integrations/commercial/test_ip_mac_address_length.py +84 -0
  86. tests/regscale/integrations/commercial/test_jira.py +2204 -0
  87. tests/regscale/integrations/commercial/test_npm_audit.py +42 -0
  88. tests/regscale/integrations/commercial/test_okta.py +1228 -0
  89. tests/regscale/integrations/commercial/test_sarif_converter.py +251 -0
  90. tests/regscale/integrations/commercial/test_sicura.py +350 -0
  91. tests/regscale/integrations/commercial/test_snow.py +423 -0
  92. tests/regscale/integrations/commercial/test_sonarcloud.py +394 -0
  93. tests/regscale/integrations/commercial/test_sqlserver.py +186 -0
  94. tests/regscale/integrations/commercial/test_stig.py +33 -0
  95. tests/regscale/integrations/commercial/test_stig_mapper.py +153 -0
  96. tests/regscale/integrations/commercial/test_stigv2.py +406 -0
  97. tests/regscale/integrations/commercial/test_wiz.py +1365 -0
  98. tests/regscale/integrations/commercial/test_wiz_inventory.py +256 -0
  99. tests/regscale/integrations/commercial/wizv2/__init__.py +339 -0
  100. tests/regscale/integrations/commercial/wizv2/compliance/__init__.py +1 -0
  101. tests/regscale/integrations/commercial/wizv2/compliance/test_helpers.py +903 -0
  102. tests/regscale/integrations/commercial/wizv2/core/__init__.py +1 -0
  103. tests/regscale/integrations/commercial/wizv2/core/test_auth.py +701 -0
  104. tests/regscale/integrations/commercial/wizv2/core/test_client.py +1037 -0
  105. tests/regscale/integrations/commercial/wizv2/core/test_file_operations.py +989 -0
  106. tests/regscale/integrations/commercial/wizv2/fetchers/__init__.py +1 -0
  107. tests/regscale/integrations/commercial/wizv2/fetchers/test_policy_assessment.py +805 -0
  108. tests/regscale/integrations/commercial/wizv2/parsers/__init__.py +1 -0
  109. tests/regscale/integrations/commercial/wizv2/parsers/test_main.py +1153 -0
  110. tests/regscale/integrations/commercial/wizv2/processors/__init__.py +1 -0
  111. tests/regscale/integrations/commercial/wizv2/processors/test_finding.py +671 -0
  112. tests/regscale/integrations/commercial/wizv2/test_WizDataMixin.py +537 -0
  113. tests/regscale/integrations/commercial/wizv2/test_click_comprehensive.py +851 -0
  114. tests/regscale/integrations/commercial/wizv2/test_compliance_report_comprehensive.py +910 -0
  115. tests/regscale/integrations/commercial/wizv2/test_compliance_report_normalization.py +138 -0
  116. tests/regscale/integrations/commercial/wizv2/test_file_cleanup.py +283 -0
  117. tests/regscale/integrations/commercial/wizv2/test_file_operations.py +260 -0
  118. tests/regscale/integrations/commercial/wizv2/test_issue.py +343 -0
  119. tests/regscale/integrations/commercial/wizv2/test_issue_comprehensive.py +1203 -0
  120. tests/regscale/integrations/commercial/wizv2/test_reports.py +497 -0
  121. tests/regscale/integrations/commercial/wizv2/test_sbom.py +643 -0
  122. tests/regscale/integrations/commercial/wizv2/test_scanner_comprehensive.py +805 -0
  123. tests/regscale/integrations/commercial/wizv2/test_wiz_click_client_id.py +165 -0
  124. tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_report.py +1394 -0
  125. tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_unit.py +341 -0
  126. tests/regscale/integrations/commercial/wizv2/test_wiz_control_normalization.py +138 -0
  127. tests/regscale/integrations/commercial/wizv2/test_wiz_findings_comprehensive.py +364 -0
  128. tests/regscale/integrations/commercial/wizv2/test_wiz_inventory_comprehensive.py +644 -0
  129. tests/regscale/integrations/commercial/wizv2/test_wiz_status_mapping.py +149 -0
  130. tests/regscale/integrations/commercial/wizv2/test_wizv2.py +1132 -0
  131. tests/regscale/integrations/commercial/wizv2/test_wizv2_utils.py +519 -0
  132. tests/regscale/integrations/commercial/wizv2/utils/__init__.py +1 -0
  133. tests/regscale/integrations/commercial/wizv2/utils/test_main.py +1523 -0
  134. tests/regscale/integrations/public/fedramp/__init__.py +1 -0
  135. tests/regscale/integrations/public/fedramp/test_poam_export_v5.py +1293 -0
  136. tests/regscale/integrations/public/test_fedramp.py +301 -0
  137. tests/regscale/integrations/test_control_matcher.py +1397 -0
  138. tests/regscale/integrations/test_control_matching.py +155 -0
  139. tests/regscale/integrations/test_milestone_manager.py +408 -0
  140. tests/regscale/models/test_issue.py +378 -1
  141. regscale/integrations/commercial/wizv2/policy_compliance.py +0 -3543
  142. /regscale/integrations/commercial/wizv2/{wiz_auth.py → core/auth.py} +0 -0
  143. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/LICENSE +0 -0
  144. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/WHEEL +0 -0
  145. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/entry_points.txt +0 -0
  146. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/top_level.txt +0 -0
@@ -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
@@ -672,6 +673,9 @@ class ScannerIntegration(ABC):
672
673
  # Initialize due date handler for this integration
673
674
  self.due_date_handler = DueDateHandler(self.title, config=self.app.config)
674
675
 
676
+ # Initialize milestone manager for this integration
677
+ self.milestone_manager = None # Lazy initialization after scan_date is set
678
+
675
679
  if self.is_component:
676
680
  self.component = regscale_models.Component.get_object(self.plan_id)
677
681
  self.parent_module: str = regscale_models.Component.get_module_string()
@@ -709,6 +713,9 @@ class ScannerIntegration(ABC):
709
713
  self._no_ccis: bool = False
710
714
  self.cci_to_control_map_lock: threading.Lock = threading.Lock()
711
715
 
716
+ # Lock for thread-safe scan history count updates
717
+ self.scan_history_lock: threading.RLock = threading.RLock()
718
+
712
719
  self.assessment_map: ThreadSafeDict[int, regscale_models.Assessment] = ThreadSafeDict()
713
720
  self.assessor_id: str = self.get_assessor_id()
714
721
  self.asset_progress: Progress = create_progress_object()
@@ -737,6 +744,21 @@ class ScannerIntegration(ABC):
737
744
  cls._lock_registry[key] = lock
738
745
  return lock
739
746
 
747
+ def get_milestone_manager(self) -> MilestoneManager:
748
+ """
749
+ Get or initialize the milestone manager.
750
+
751
+ :return: MilestoneManager instance
752
+ :rtype: MilestoneManager
753
+ """
754
+ if self.milestone_manager is None:
755
+ self.milestone_manager = MilestoneManager(
756
+ integration_title=self.title,
757
+ assessor_id=self.assessor_id,
758
+ scan_date=self.scan_date or get_current_datetime(),
759
+ )
760
+ return self.milestone_manager
761
+
740
762
  @staticmethod
741
763
  def load_stig_mapper() -> Optional[STIGMapper]:
742
764
  """
@@ -997,15 +1019,18 @@ class ScannerIntegration(ABC):
997
1019
  return res[:450]
998
1020
  return prefix[:450]
999
1021
 
1000
- def get_or_create_assessment(self, control_implementation_id: int) -> regscale_models.Assessment:
1022
+ def get_or_create_assessment(
1023
+ self, control_implementation_id: int, status: Optional[regscale_models.AssessmentResultsStatus] = None
1024
+ ) -> regscale_models.Assessment:
1001
1025
  """
1002
- Gets or creates a RegScale assessment
1026
+ Gets or creates a RegScale assessment.
1003
1027
 
1004
1028
  :param int control_implementation_id: The ID of the control implementation
1029
+ :param Optional[regscale_models.AssessmentResultsStatus] status: Optional status override (used by cci_assessment)
1005
1030
  :return: The assessment
1006
1031
  :rtype: regscale_models.Assessment
1007
1032
  """
1008
- logger.info("Getting or create assessment for control implementation %d", control_implementation_id)
1033
+ logger.debug("Getting or create assessment for control implementation %d", control_implementation_id)
1009
1034
  assessment: Optional[regscale_models.Assessment] = self.assessment_map.get(control_implementation_id)
1010
1035
  if assessment:
1011
1036
  logger.debug(
@@ -1017,7 +1042,7 @@ class ScannerIntegration(ABC):
1017
1042
  plannedStart=get_current_datetime(),
1018
1043
  plannedFinish=get_current_datetime(),
1019
1044
  status=regscale_models.AssessmentStatus.COMPLETE.value,
1020
- assessmentResult=regscale_models.AssessmentResultsStatus.FAIL.value,
1045
+ assessmentResult=status.value if status else regscale_models.AssessmentResultsStatus.FAIL.value,
1021
1046
  actualFinish=get_current_datetime(),
1022
1047
  leadAssessorId=self.assessor_id,
1023
1048
  parentId=control_implementation_id,
@@ -2284,87 +2309,26 @@ class ScannerIntegration(ABC):
2284
2309
  """
2285
2310
  Create milestones for an issue based on status transitions.
2286
2311
 
2312
+ Delegates to MilestoneManager for cleaner separation of concerns.
2313
+ Also ensures existing issues have creation milestones (backfills if missing).
2314
+
2287
2315
  :param regscale_models.Issue issue: The issue to create milestones for
2288
2316
  :param IntegrationFinding finding: The finding data
2289
2317
  :param Optional[regscale_models.Issue] existing_issue: Existing issue for comparison
2290
2318
  """
2291
- if not (ScannerVariables.useMilestones and issue.id):
2292
- return
2293
-
2294
- if self._should_create_reopened_milestone(existing_issue, issue):
2295
- self._create_milestone_safe(
2296
- issue, finding, "Issue reopened from", get_current_datetime(), "reopened milestone"
2297
- )
2298
- elif self._should_create_closed_milestone(existing_issue, issue):
2299
- self._create_milestone_safe(issue, finding, "Issue closed from", issue.dateCompleted, "closed milestone")
2300
- elif not existing_issue:
2301
- self._create_milestone_safe(issue, finding, "Issue created from", self.scan_date, "new issue milestone")
2302
- else:
2303
- logger.debug("No milestone created for issue %s from finding %s", issue.id, finding.external_id)
2304
-
2305
- def _should_create_reopened_milestone(
2306
- self, existing_issue: Optional[regscale_models.Issue], issue: regscale_models.Issue
2307
- ) -> bool:
2308
- """
2309
- Check if a reopened milestone should be created.
2319
+ milestone_manager = self.get_milestone_manager()
2310
2320
 
2311
- :param Optional[regscale_models.Issue] existing_issue: The existing issue
2312
- :param regscale_models.Issue issue: The current issue
2313
- :return: True if reopened milestone should be created
2314
- :rtype: bool
2315
- """
2316
- return (
2317
- existing_issue
2318
- and existing_issue.status == regscale_models.IssueStatus.Closed
2319
- and issue.status == regscale_models.IssueStatus.Open
2320
- )
2321
-
2322
- def _should_create_closed_milestone(
2323
- self, existing_issue: Optional[regscale_models.Issue], issue: regscale_models.Issue
2324
- ) -> bool:
2325
- """
2326
- Check if a closed milestone should be created.
2321
+ # For existing issues, ensure they have a creation milestone (backfill if missing)
2322
+ if existing_issue:
2323
+ milestone_manager.ensure_creation_milestone_exists(issue=issue, finding=finding)
2327
2324
 
2328
- :param Optional[regscale_models.Issue] existing_issue: The existing issue
2329
- :param regscale_models.Issue issue: The current issue
2330
- :return: True if closed milestone should be created
2331
- :rtype: bool
2332
- """
2333
- return (
2334
- existing_issue
2335
- and existing_issue.status == regscale_models.IssueStatus.Open
2336
- and issue.status == regscale_models.IssueStatus.Closed
2325
+ # Handle status transition milestones
2326
+ milestone_manager.create_milestones_for_issue(
2327
+ issue=issue,
2328
+ finding=finding,
2329
+ existing_issue=existing_issue,
2337
2330
  )
2338
2331
 
2339
- def _create_milestone_safe(
2340
- self,
2341
- issue: regscale_models.Issue,
2342
- finding: IntegrationFinding,
2343
- title_prefix: str,
2344
- milestone_date: str,
2345
- milestone_type: str,
2346
- ) -> None:
2347
- """
2348
- Safely create a milestone with error handling.
2349
-
2350
- :param regscale_models.Issue issue: The issue to create milestone for
2351
- :param IntegrationFinding finding: The finding data
2352
- :param str title_prefix: Prefix for milestone title
2353
- :param str milestone_date: Date for the milestone
2354
- :param str milestone_type: Description for logging purposes
2355
- """
2356
- try:
2357
- regscale_models.Milestone(
2358
- title=f"{title_prefix} {self.title} scan",
2359
- milestoneDate=milestone_date,
2360
- responsiblePersonId=self.assessor_id,
2361
- parentID=issue.id,
2362
- parentModule="issues",
2363
- ).create_or_update()
2364
- logger.debug("Added milestone for issue %s from finding %s", issue.id, finding.external_id)
2365
- except Exception as e:
2366
- logger.warning("Failed to create %s: %s", milestone_type, str(e))
2367
-
2368
2332
  @staticmethod
2369
2333
  def extra_data_to_properties(finding: IntegrationFinding, issue_id: int) -> None:
2370
2334
  """
@@ -2530,6 +2494,8 @@ class ScannerIntegration(ABC):
2530
2494
  if found_issue.controlImplementationIds:
2531
2495
  for control_id in found_issue.controlImplementationIds:
2532
2496
  self.update_control_implementation_status_after_close(control_id)
2497
+ # Update assessment status to reflect the control implementation status
2498
+ self.update_assessment_status_from_control_implementation(control_id)
2533
2499
 
2534
2500
  def handle_failing_checklist(
2535
2501
  self,
@@ -2554,11 +2520,13 @@ class ScannerIntegration(ABC):
2554
2520
  if failing_objective.name.lower().startswith("cci-"):
2555
2521
  implementation_id = self.get_control_implementation_id_for_cci(failing_objective.name)
2556
2522
  else:
2557
- control_label = objective_to_control_dot(failing_objective.name)
2558
- if control_label not in self.control_implementation_id_map:
2559
- logger.warning("Control %s not found for %s", control_label, control_label)
2560
- continue
2561
- implementation_id = self.control_implementation_id_map[control_label]
2523
+ implementation_id = self._fallback_implementation_id(failing_objective)
2524
+
2525
+ if not implementation_id or implementation_id is None:
2526
+ logger.warning(
2527
+ "Could not map objective to a Control Implementation for objective #%i.", failing_objective.id
2528
+ )
2529
+ continue
2562
2530
 
2563
2531
  failing_option = regscale_models.ImplementationOption(
2564
2532
  name="Failed STIG",
@@ -2580,13 +2548,36 @@ class ScannerIntegration(ABC):
2580
2548
  ).create_or_update()
2581
2549
 
2582
2550
  # Create assessment and control test result
2583
- assessment = self.get_or_create_assessment(implementation_id)
2551
+ assessment = self.get_or_create_assessment(
2552
+ implementation_id, status=regscale_models.AssessmentResultsStatus.FAIL
2553
+ )
2584
2554
  if implementation_id:
2585
2555
  control_test = self.create_or_get_control_test(finding, implementation_id)
2586
2556
  self.create_control_test_result(
2587
2557
  finding, control_test, assessment, regscale_models.ControlTestResultStatus.FAIL
2588
2558
  )
2589
2559
 
2560
+ def _fallback_implementation_id(self, objective: regscale_models.ControlObjective) -> Optional[int]:
2561
+ """
2562
+ Fallback method to get control implementation ID from objective name if CCI mapping fails.
2563
+
2564
+ :param regscale_models.ControlObjective objective: The control objective
2565
+ :return: The control implementation ID if found, None otherwise
2566
+ :rtype: Optional[int]
2567
+ """
2568
+ control_label = objective_to_control_dot(objective.name)
2569
+ if implementation_id := self.control_implementation_id_map.get(control_label):
2570
+ return implementation_id
2571
+
2572
+ if control_id := self.control_id_to_implementation_map.get(objective.securityControlId):
2573
+ if control_label := self.control_map.get(control_id):
2574
+ implementation_id = self.control_implementation_id_map.get(control_label)
2575
+ if not implementation_id:
2576
+ print("No dice.")
2577
+ return implementation_id
2578
+ logger.debug("Could not find fallback implementation ID for objective #%i", objective.id)
2579
+ return None
2580
+
2590
2581
  def handle_passing_checklist(
2591
2582
  self,
2592
2583
  finding: IntegrationFinding,
@@ -2610,15 +2601,12 @@ class ScannerIntegration(ABC):
2610
2601
  if passing_objective.name.lower().startswith("cci-"):
2611
2602
  implementation_id = self.get_control_implementation_id_for_cci(passing_objective.name)
2612
2603
  else:
2613
- control_label = objective_to_control_dot(passing_objective.name)
2614
- if control_label not in self.control_implementation_id_map:
2615
- logger.warning("Control %s not found for %s", control_label, control_label)
2616
- continue
2617
- implementation_id = self.control_implementation_id_map[control_label]
2618
-
2619
- # Skip if we couldn't determine the implementation ID
2620
- if implementation_id is None:
2621
- logger.warning("Could not determine implementation ID for objective %s", passing_objective.name)
2604
+ implementation_id = self._fallback_implementation_id(passing_objective)
2605
+
2606
+ if not implementation_id or implementation_id is None:
2607
+ logger.warning(
2608
+ "Could not map objective to a Control Implementation for objective #%i.", passing_objective.id
2609
+ )
2622
2610
  continue
2623
2611
 
2624
2612
  passing_option = regscale_models.ImplementationOption(
@@ -2641,7 +2629,9 @@ class ScannerIntegration(ABC):
2641
2629
  ).create_or_update()
2642
2630
 
2643
2631
  # Create assessment and control test result
2644
- assessment = self.get_or_create_assessment(implementation_id)
2632
+ assessment = self.get_or_create_assessment(
2633
+ implementation_id, status=regscale_models.AssessmentResultsStatus.PASS
2634
+ )
2645
2635
  control_test = self.create_or_get_control_test(finding, implementation_id)
2646
2636
  self.create_control_test_result(
2647
2637
  finding, control_test, assessment, regscale_models.ControlTestResultStatus.PASS
@@ -2667,17 +2657,45 @@ class ScannerIntegration(ABC):
2667
2657
 
2668
2658
  def get_asset_by_identifier(self, identifier: str) -> Optional[regscale_models.Asset]:
2669
2659
  """
2670
- Gets an asset by its identifier
2660
+ Gets an asset by its identifier with fallback lookups.
2661
+
2662
+ REG-17044: Enhanced to support multiple identifier fields (qualysId, IP, FQDN)
2663
+ to improve asset matching and reduce "asset not found" errors.
2671
2664
 
2672
2665
  :param str identifier: The identifier of the asset
2673
2666
  :return: The asset
2674
2667
  :rtype: Optional[regscale_models.Asset]
2675
2668
  """
2676
- asset = self.asset_map_by_identifier.get(identifier)
2669
+ # Try primary identifier field first
2670
+ if asset := self.asset_map_by_identifier.get(identifier):
2671
+ return asset
2672
+
2673
+ # Fallback: Try common identifier fields
2674
+ # This helps when asset_identifier_field doesn't match or assets use different identifiers
2675
+ if not asset and identifier:
2676
+ for cached_asset in self.asset_map_by_identifier.values():
2677
+ # Try IP address lookup
2678
+ if getattr(cached_asset, "ipAddress", None) == identifier:
2679
+ logger.debug(f"Found asset {cached_asset.id} by IP address fallback: {identifier}")
2680
+ return cached_asset
2681
+ # Try FQDN lookup
2682
+ if getattr(cached_asset, "fqdn", None) == identifier:
2683
+ logger.debug(f"Found asset {cached_asset.id} by FQDN fallback: {identifier}")
2684
+ return cached_asset
2685
+ # Try DNS lookup
2686
+ if getattr(cached_asset, "dns", None) == identifier:
2687
+ logger.debug(f"Found asset {cached_asset.id} by DNS fallback: {identifier}")
2688
+ return cached_asset
2689
+
2690
+ # Log error if still not found
2677
2691
  if not asset and identifier not in self.alerted_assets:
2678
2692
  self.alerted_assets.add(identifier)
2679
2693
  if not getattr(self, "suppress_asset_not_found_errors", False):
2680
- self.log_error("1. Asset not found for identifier %s", identifier)
2694
+ self.log_error(
2695
+ "Asset not found for identifier '%s' (tried %s, ipAddress, fqdn, dns)",
2696
+ identifier,
2697
+ self.asset_identifier_field,
2698
+ )
2681
2699
  return asset
2682
2700
 
2683
2701
  def get_issue_by_integration_finding_id(self, integration_finding_id: str) -> Optional[regscale_models.Issue]:
@@ -2706,7 +2724,11 @@ class ScannerIntegration(ABC):
2706
2724
  logger.error("2. Asset not found for identifier %s", finding.asset_identifier)
2707
2725
  return 0
2708
2726
 
2709
- tool = regscale_models.ChecklistTool.STIGs
2727
+ tool = (
2728
+ regscale_models.ChecklistTool.CISBenchmarks
2729
+ if "simp.cis" in str(finding.vulnerability_number).lower()
2730
+ else regscale_models.ChecklistTool.STIGs
2731
+ )
2710
2732
  if finding.vulnerability_type == "Vulnerability Scan":
2711
2733
  tool = regscale_models.ChecklistTool.VulnerabilityScanner
2712
2734
 
@@ -2871,7 +2893,7 @@ class ScannerIntegration(ABC):
2871
2893
  def _finalize_finding_processing(
2872
2894
  self, scan_history: regscale_models.ScanHistory, current_vulnerabilities: Dict[int, Set[int]]
2873
2895
  ) -> None:
2874
- """Finalize the finding processing by saving scan history and closing outdated issues."""
2896
+ """Finalize the finding processing by saving scan history and closing outdated vulnerabilities and issues."""
2875
2897
  logger.info(
2876
2898
  f"Saving scan history with final counts - Low: {scan_history.vLow}, Medium: {scan_history.vMedium}, High: {scan_history.vHigh}, Critical: {scan_history.vCritical}, Info: {scan_history.vInfo}"
2877
2899
  )
@@ -2890,6 +2912,7 @@ class ScannerIntegration(ABC):
2890
2912
 
2891
2913
  self._results["scan_history"] = scan_history
2892
2914
  self.update_result_counts("issues", regscale_models.Issue.bulk_save(progress_context=self.finding_progress))
2915
+ self.close_outdated_vulnerabilities(current_vulnerabilities)
2893
2916
  self.close_outdated_issues(current_vulnerabilities)
2894
2917
  self._perform_batch_operations(self.finding_progress)
2895
2918
 
@@ -2995,24 +3018,25 @@ class ScannerIntegration(ABC):
2995
3018
  self._process_checklist_finding(finding)
2996
3019
 
2997
3020
  # Process vulnerability if applicable
2998
- if finding.status != regscale_models.IssueStatus.Closed or ScannerVariables.ingestClosedIssues:
2999
- vulnerability_created = self._process_vulnerability_finding(finding, scan_history, current_vulnerabilities)
3021
+ # IMPORTANT: Always track vulnerabilities regardless of status to enable proper issue closure logic
3022
+ # This ensures that current_vulnerabilities dict accurately reflects the scan state
3023
+ vulnerability_created = self._process_vulnerability_finding(finding, scan_history, current_vulnerabilities)
3000
3024
 
3025
+ # Only create/update issues for non-closed findings (unless ingestClosedIssues is enabled)
3026
+ if finding.status != regscale_models.IssueStatus.Closed or ScannerVariables.ingestClosedIssues:
3001
3027
  self.handle_failing_finding(
3002
3028
  issue_title=finding.issue_title or finding.title,
3003
3029
  finding=finding,
3004
3030
  )
3005
3031
 
3006
- # Update scan history severity counts only if vulnerability was successfully created
3007
- if vulnerability_created:
3008
- logger.debug(
3009
- f"Updating severity count for successfully created vulnerability with severity: {finding.severity}"
3010
- )
3011
- self.set_severity_count_for_scan(finding.severity, scan_history)
3012
- else:
3013
- logger.debug(
3014
- f"Skipping severity count update for finding {finding.external_id} - no vulnerability created"
3015
- )
3032
+ # Update scan history severity counts only if vulnerability was successfully created
3033
+ if vulnerability_created:
3034
+ logger.debug(
3035
+ f"Updating severity count for successfully created vulnerability with severity: {finding.severity}"
3036
+ )
3037
+ self.set_severity_count_for_scan(finding.severity, scan_history, self.scan_history_lock)
3038
+ else:
3039
+ logger.debug(f"Skipping severity count update for finding {finding.external_id} - no vulnerability created")
3016
3040
 
3017
3041
  def _process_checklist_finding(self, finding: IntegrationFinding) -> None:
3018
3042
  """Process a checklist finding."""
@@ -3269,13 +3293,34 @@ class ScannerIntegration(ABC):
3269
3293
  vuln_list.append(vuln)
3270
3294
  return vuln_list
3271
3295
 
3272
- def close_outdated_vulnerabilities(self, current_vulnerabilities: Dict[int, Set[int]]) -> None:
3296
+ def close_outdated_vulnerabilities(self, current_vulnerabilities: Dict[int, Set[int]]) -> int:
3273
3297
  """
3274
3298
  Closes vulnerabilities that are not in the current set of vulnerability IDs for each asset.
3275
3299
 
3276
3300
  :param Dict[int, Set[int]] current_vulnerabilities: Dictionary of asset IDs to lists of current vulnerability IDs
3277
- :rtype: None
3301
+ :return: Number of vulnerabilities closed
3302
+ :rtype: int
3278
3303
  """
3304
+ if not self.close_outdated_findings:
3305
+ logger.info("Skipping closing outdated vulnerabilities.")
3306
+ return 0
3307
+
3308
+ # Check global preventAutoClose setting
3309
+ from regscale.core.app.application import Application
3310
+
3311
+ app = Application()
3312
+ if app.config.get("preventAutoClose", False):
3313
+ logger.info("Skipping closing outdated vulnerabilities due to global preventAutoClose setting.")
3314
+ return 0
3315
+
3316
+ # REG-17044: Add defensive logging to track vulnerability closure state
3317
+ logger.debug(f"Vulnerability Closure Analysis for {self.title}:")
3318
+ logger.debug(f" - Assets with current vulnerabilities: {len(current_vulnerabilities)}")
3319
+ total_current_vulns = sum(len(vuln_set) for vuln_set in current_vulnerabilities.values())
3320
+ logger.debug(f" - Total current vulnerabilities tracked: {total_current_vulns}")
3321
+ if total_current_vulns == 0:
3322
+ logger.warning("No current vulnerabilities tracked - this may close all vulnerabilities!")
3323
+
3279
3324
  # Get all current vulnerability IDs
3280
3325
  current_vuln_ids = {vuln_id for vuln_ids in current_vulnerabilities.values() for vuln_id in vuln_ids}
3281
3326
 
@@ -3298,9 +3343,14 @@ class ScannerIntegration(ABC):
3298
3343
  vuln.dateClosed = get_current_datetime()
3299
3344
  vuln.save()
3300
3345
  closed_count += 1
3301
- logger.info("Closed vulnerability %d", vuln.id)
3346
+ logger.debug("Closed vulnerability %d", vuln.id)
3302
3347
 
3303
- logger.info("Closed %d outdated vulnerabilities.", closed_count)
3348
+ (
3349
+ logger.info("Closed %d outdated vulnerabilities.", closed_count)
3350
+ if closed_count > 0
3351
+ else logger.info("No outdated vulnerabilities to close.")
3352
+ )
3353
+ return closed_count
3304
3354
 
3305
3355
  @classmethod
3306
3356
  def close_mappings_list(cls, vuln: regscale_models.Vulnerability) -> None:
@@ -3350,6 +3400,13 @@ class ScannerIntegration(ABC):
3350
3400
  logger.info("Skipping closing outdated issues due to global preventAutoClose setting.")
3351
3401
  return 0
3352
3402
 
3403
+ # REG-17044: Add defensive logging to track issue closure state
3404
+ logger.debug(f"Issue Closure Analysis for {self.title}:")
3405
+ total_current_vulns = sum(len(vuln_set) for vuln_set in current_vulnerabilities.values())
3406
+ logger.debug(f" - Total current vulnerabilities to check against: {total_current_vulns}")
3407
+ if total_current_vulns == 0:
3408
+ logger.warning("No current vulnerabilities tracked - this may close all issues!")
3409
+
3353
3410
  closed_count = 0
3354
3411
  affected_control_ids = set()
3355
3412
  count_lock = threading.Lock()
@@ -3381,6 +3438,8 @@ class ScannerIntegration(ABC):
3381
3438
 
3382
3439
  for control_id in affected_control_ids:
3383
3440
  self.update_control_implementation_status_after_close(control_id)
3441
+ # Update assessment status to reflect the control implementation status
3442
+ self.update_assessment_status_from_control_implementation(control_id)
3384
3443
 
3385
3444
  (
3386
3445
  logger.info("Closed %d outdated issues.", closed_count)
@@ -3483,9 +3542,70 @@ class ScannerIntegration(ABC):
3483
3542
  if control_implementation.status != new_status:
3484
3543
  control_implementation.status = new_status
3485
3544
  self.control_implementation_map[control_id] = control_implementation.save()
3486
- logger.info("Updated control implementation %d status to %s", control_id, new_status)
3545
+ logger.debug("Updated control implementation %d status to %s", control_id, new_status)
3487
3546
 
3488
- def is_issue_protected_from_auto_close(self, issue: regscale_models.Issue) -> bool:
3547
+ def update_assessment_status_from_control_implementation(self, control_implementation_id: int) -> None:
3548
+ """
3549
+ Updates the assessment status based on the control implementation status.
3550
+ Treats the ControlImplementation status as the source of truth.
3551
+
3552
+ Sets assessment to PASS if ControlImplementation status is FULLY_IMPLEMENTED,
3553
+ otherwise sets it to FAIL.
3554
+
3555
+ This method should be called after update_control_implementation_status_after_close
3556
+ to ensure assessments reflect the final control implementation state.
3557
+
3558
+ :param int control_implementation_id: The ID of the control implementation
3559
+ :rtype: None
3560
+ """
3561
+ # Get the cached assessment for this control implementation
3562
+ assessment = self.assessment_map.get(control_implementation_id)
3563
+
3564
+ if not assessment:
3565
+ logger.debug(
3566
+ "No assessment found in cache for control implementation %d, skipping assessment update",
3567
+ control_implementation_id,
3568
+ )
3569
+ return
3570
+
3571
+ # Get the control implementation to check its status
3572
+ control_implementation = self.control_implementation_map.get(
3573
+ control_implementation_id
3574
+ ) or regscale_models.ControlImplementation.get_object(object_id=control_implementation_id)
3575
+
3576
+ if not control_implementation:
3577
+ logger.warning("Control implementation %d not found, cannot update assessment", control_implementation_id)
3578
+ return
3579
+
3580
+ # Determine assessment result based on control implementation status
3581
+ # Treat ControlImplementation status as the source of truth
3582
+ new_assessment_result = (
3583
+ regscale_models.AssessmentResultsStatus.PASS
3584
+ if control_implementation.status == regscale_models.ImplementationStatus.FULLY_IMPLEMENTED.value
3585
+ else regscale_models.AssessmentResultsStatus.FAIL
3586
+ )
3587
+
3588
+ # Only update if the status has changed
3589
+ if assessment.assessmentResult != new_assessment_result.value:
3590
+ assessment.assessmentResult = new_assessment_result.value
3591
+ assessment.save()
3592
+ logger.debug(
3593
+ "Updated assessment %d for control implementation %d: assessmentResult=%s (based on control status: %s)",
3594
+ assessment.id,
3595
+ control_implementation_id,
3596
+ new_assessment_result.value,
3597
+ control_implementation.status,
3598
+ )
3599
+ else:
3600
+ logger.debug(
3601
+ "Assessment %d already has correct status %s for control implementation %d",
3602
+ assessment.id,
3603
+ assessment.assessmentResult,
3604
+ control_implementation_id,
3605
+ )
3606
+
3607
+ @staticmethod
3608
+ def is_issue_protected_from_auto_close(issue: regscale_models.Issue) -> bool:
3489
3609
  """
3490
3610
  Check if an issue is protected from automatic closure.
3491
3611
 
@@ -3567,51 +3687,55 @@ class ScannerIntegration(ABC):
3567
3687
  return True
3568
3688
 
3569
3689
  @staticmethod
3570
- def set_severity_count_for_scan(severity: str, scan_history: regscale_models.ScanHistory) -> None:
3690
+ def set_severity_count_for_scan(
3691
+ severity: str, scan_history: regscale_models.ScanHistory, lock: Optional[threading.RLock] = None
3692
+ ) -> None:
3571
3693
  """
3572
- Increments the count of the severity
3694
+ Increments the count of the severity in a thread-safe manner.
3695
+
3696
+ NOTE: This method does NOT save the scan_history object. The caller is responsible
3697
+ for saving the scan_history after all increments are complete to avoid race conditions
3698
+ and excessive database writes in multi-threaded environments.
3699
+
3573
3700
  :param str severity: Severity of the vulnerability
3574
3701
  :param regscale_models.ScanHistory scan_history: Scan history object
3702
+ :param Optional[threading.RLock] lock: Thread lock for synchronization (recommended in multi-threaded context)
3575
3703
  :rtype: None
3576
3704
  """
3577
- logger.debug(f"Setting severity count for scan {scan_history.id}: severity='{severity}'")
3578
- logger.debug(
3579
- f"Current counts - Low: {scan_history.vLow}, Medium: {scan_history.vMedium}, High: {scan_history.vHigh}, Critical: {scan_history.vCritical}, Info: {scan_history.vInfo}"
3580
- )
3581
3705
 
3582
- if severity == regscale_models.IssueSeverity.Low.value:
3583
- scan_history.vLow += 1
3584
- logger.debug(f"Incremented vLow count to {scan_history.vLow}")
3585
- elif severity == regscale_models.IssueSeverity.Moderate.value:
3586
- scan_history.vMedium += 1
3587
- logger.debug(f"Incremented vMedium count to {scan_history.vMedium}")
3588
- elif severity == regscale_models.IssueSeverity.High.value:
3589
- scan_history.vHigh += 1
3590
- logger.debug(f"Incremented vHigh count to {scan_history.vHigh}")
3591
- elif severity == regscale_models.IssueSeverity.Critical.value:
3592
- scan_history.vCritical += 1
3593
- logger.debug(f"Incremented vCritical count to {scan_history.vCritical}")
3594
- else:
3595
- scan_history.vInfo += 1
3596
- logger.debug(f"Incremented vInfo count to {scan_history.vInfo}")
3706
+ def _increment_severity():
3707
+ """Internal method to perform the actual increment."""
3708
+ logger.debug(f"Setting severity count for scan {scan_history.id}: severity='{severity}'")
3709
+ logger.debug(
3710
+ f"Current counts - Low: {scan_history.vLow}, Medium: {scan_history.vMedium}, High: {scan_history.vHigh}, Critical: {scan_history.vCritical}, Info: {scan_history.vInfo}"
3711
+ )
3597
3712
 
3598
- logger.debug(
3599
- f"Final counts - Low: {scan_history.vLow}, Medium: {scan_history.vMedium}, High: {scan_history.vHigh}, Critical: {scan_history.vCritical}, Info: {scan_history.vInfo}"
3600
- )
3713
+ if severity.lower() == regscale_models.IssueSeverity.Low.value.lower():
3714
+ scan_history.vLow += 1
3715
+ logger.debug(f"Incremented vLow count to {scan_history.vLow}")
3716
+ elif severity.lower() == regscale_models.IssueSeverity.Moderate.value.lower():
3717
+ scan_history.vMedium += 1
3718
+ logger.debug(f"Incremented vMedium count to {scan_history.vMedium}")
3719
+ elif severity.lower() == regscale_models.IssueSeverity.High.value.lower():
3720
+ scan_history.vHigh += 1
3721
+ logger.debug(f"Incremented vHigh count to {scan_history.vHigh}")
3722
+ elif severity.lower() == regscale_models.IssueSeverity.Critical.value.lower():
3723
+ scan_history.vCritical += 1
3724
+ logger.debug(f"Incremented vCritical count to {scan_history.vCritical}")
3725
+ else:
3726
+ scan_history.vInfo += 1
3727
+ logger.debug(f"Incremented vInfo count to {scan_history.vInfo}")
3601
3728
 
3602
- # Save the scan history immediately to persist the count changes
3603
- try:
3604
- scan_history.save()
3605
- logger.debug(f"Successfully saved scan history {scan_history.id} with updated counts")
3606
- except Exception as e:
3607
- logger.error(f"Error saving scan history {scan_history.id} after updating counts: {e}")
3608
- # Try to save again with a fresh fetch
3609
- try:
3610
- scan_history.fetch()
3611
- scan_history.save()
3612
- logger.debug(f"Successfully saved scan history {scan_history.id} after retry")
3613
- except Exception as e2:
3614
- logger.error(f"Failed to save scan history {scan_history.id} after retry: {e2}")
3729
+ logger.debug(
3730
+ f"Updated counts - Low: {scan_history.vLow}, Medium: {scan_history.vMedium}, High: {scan_history.vHigh}, Critical: {scan_history.vCritical}, Info: {scan_history.vInfo}"
3731
+ )
3732
+
3733
+ # Use lock if provided for thread-safe increments
3734
+ if lock:
3735
+ with lock:
3736
+ _increment_severity()
3737
+ else:
3738
+ _increment_severity()
3615
3739
 
3616
3740
  @classmethod
3617
3741
  def cci_assessment(cls, plan_id: int) -> None: