regscale-cli 6.24.0.0__py3-none-any.whl → 6.24.0.1__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.

@@ -25,6 +25,8 @@ from regscale.models.regscale_models import (
25
25
  ControlImplementation,
26
26
  Assessment,
27
27
  ImplementationObjective,
28
+ SecurityPlan,
29
+ ComplianceSettings,
28
30
  )
29
31
 
30
32
  logger = logging.getLogger("regscale")
@@ -160,6 +162,10 @@ class ComplianceIntegration(ScannerIntegration, ABC):
160
162
  # Set scan date
161
163
  self.scan_date = get_current_datetime()
162
164
 
165
+ # Cache for compliance settings
166
+ self._compliance_settings = None
167
+ self._security_plan = None
168
+
163
169
  def is_poam(self, finding: IntegrationFinding) -> bool: # type: ignore[override]
164
170
  """
165
171
  Determines if an issue should be considered a POAM for compliance integrations.
@@ -1387,6 +1393,7 @@ class ComplianceIntegration(ScannerIntegration, ABC):
1387
1393
  def _parse_control_id(control_id: str) -> tuple[str, Optional[str]]:
1388
1394
  """
1389
1395
  Parse a control id like 'AC-2(1)', 'AC-2 (1)', 'AC-2-1' into (base, sub).
1396
+ Normalizes leading zeros (e.g., AC-01 becomes AC-1).
1390
1397
 
1391
1398
  Returns (base, None) when no subcontrol.
1392
1399
 
@@ -1400,8 +1407,22 @@ class ComplianceIntegration(ScannerIntegration, ABC):
1400
1407
  if not m:
1401
1408
  return cid.upper(), None
1402
1409
  base = m.group(1).upper()
1410
+ # Normalize leading zeros in base control number (e.g., AC-01 -> AC-1)
1411
+ if "-" in base:
1412
+ prefix, number = base.split("-", 1)
1413
+ try:
1414
+ normalized_number = str(int(number))
1415
+ base = f"{prefix}-{normalized_number}"
1416
+ except ValueError:
1417
+ pass # Keep original if conversion fails
1403
1418
  # Subcontrol may be captured in group 2, 3, or 4 depending on the branch matched
1404
1419
  sub = m.group(2) or m.group(3) or m.group(4)
1420
+ # Normalize leading zeros in subcontrol (e.g., 01 -> 1)
1421
+ if sub:
1422
+ try:
1423
+ sub = str(int(sub))
1424
+ except ValueError:
1425
+ pass # Keep original if conversion fails
1405
1426
  return base, sub
1406
1427
 
1407
1428
  @classmethod
@@ -1433,6 +1454,7 @@ class ComplianceIntegration(ScannerIntegration, ABC):
1433
1454
  def _normalize_control_id(control_id: str) -> tuple[str, Optional[str]]:
1434
1455
  """
1435
1456
  Normalize control id to a canonical tuple (BASE, SUB) for set membership.
1457
+ Normalizes leading zeros (e.g., AC-01 becomes AC-1).
1436
1458
 
1437
1459
  :param str control_id: Control identifier to normalize
1438
1460
  :return: Tuple of (base_control, subcontrol) in canonical form
@@ -1444,7 +1466,21 @@ class ComplianceIntegration(ScannerIntegration, ABC):
1444
1466
  if not m:
1445
1467
  return cid.upper(), None
1446
1468
  base = m.group(1).upper()
1469
+ # Normalize leading zeros in base control number (e.g., AC-01 -> AC-1)
1470
+ if "-" in base:
1471
+ prefix, number = base.split("-", 1)
1472
+ try:
1473
+ normalized_number = str(int(number))
1474
+ base = f"{prefix}-{normalized_number}"
1475
+ except ValueError:
1476
+ pass # Keep original if conversion fails
1447
1477
  sub = m.group(2) or m.group(3) or m.group(4)
1478
+ # Normalize leading zeros in subcontrol (e.g., 01 -> 1)
1479
+ if sub:
1480
+ try:
1481
+ sub = str(int(sub))
1482
+ except ValueError:
1483
+ pass # Keep original if conversion fails
1448
1484
  return base, sub
1449
1485
 
1450
1486
  def _create_control_assessment(
@@ -1622,9 +1658,198 @@ class ComplianceIntegration(ScannerIntegration, ABC):
1622
1658
 
1623
1659
  return "\n".join(html_parts)
1624
1660
 
1661
+ def _get_security_plan(self) -> Optional[regscale_models.SecurityPlan]:
1662
+ """
1663
+ Get the security plan for this integration.
1664
+
1665
+ :return: SecurityPlan instance or None
1666
+ :rtype: Optional[regscale_models.SecurityPlan]
1667
+ """
1668
+ if self._security_plan is None:
1669
+ try:
1670
+ logger.debug(f"Retrieving security plan with ID: {self.plan_id}")
1671
+ self._security_plan = SecurityPlan.get_object(object_id=self.plan_id)
1672
+ if self._security_plan:
1673
+ logger.debug(f"Retrieved security plan: {self._security_plan.title}")
1674
+ logger.debug(f"complianceSettingsId: {getattr(self._security_plan, 'complianceSettingsId', None)}")
1675
+ else:
1676
+ logger.debug(f"No security plan found with ID: {self.plan_id}")
1677
+ except Exception as e:
1678
+ logger.debug(f"Error getting security plan {self.plan_id}: {e}")
1679
+ self._security_plan = None
1680
+ return self._security_plan
1681
+
1682
+ def _get_compliance_settings(self) -> Optional[regscale_models.ComplianceSettings]:
1683
+ """
1684
+ Get compliance settings for the security plan.
1685
+
1686
+ :return: ComplianceSettings instance or None
1687
+ :rtype: Optional[regscale_models.ComplianceSettings]
1688
+ """
1689
+ if self._compliance_settings is None:
1690
+ try:
1691
+ security_plan = self._get_security_plan()
1692
+ if self._has_valid_compliance_settings_id(security_plan):
1693
+ self._compliance_settings = self._fetch_compliance_settings(security_plan)
1694
+ else:
1695
+ self._log_missing_compliance_settings_reason(security_plan)
1696
+ except Exception as e:
1697
+ logger.debug(f"Error getting compliance settings: {e}")
1698
+ import traceback
1699
+
1700
+ logger.debug(f"Full traceback: {traceback.format_exc()}")
1701
+ self._compliance_settings = None
1702
+ return self._compliance_settings
1703
+
1704
+ def _has_valid_compliance_settings_id(self, security_plan) -> bool:
1705
+ """Check if security plan has valid compliance settings ID."""
1706
+ return security_plan and hasattr(security_plan, "complianceSettingsId") and security_plan.complianceSettingsId
1707
+
1708
+ def _fetch_compliance_settings(self, security_plan) -> Optional[regscale_models.ComplianceSettings]:
1709
+ """Fetch and log compliance settings."""
1710
+ logger.debug(f"Retrieving compliance settings with ID: {security_plan.complianceSettingsId}")
1711
+ compliance_settings = ComplianceSettings.get_object(object_id=security_plan.complianceSettingsId)
1712
+
1713
+ if compliance_settings:
1714
+ logger.debug(f"Using compliance settings: {compliance_settings.title}")
1715
+ logger.debug(
1716
+ f"Compliance settings has field groups: {bool(getattr(compliance_settings, 'complianceSettingsFieldGroups', None))}"
1717
+ )
1718
+ else:
1719
+ logger.debug(f"No compliance settings found for ID: {security_plan.complianceSettingsId}")
1720
+
1721
+ return compliance_settings
1722
+
1723
+ def _log_missing_compliance_settings_reason(self, security_plan) -> None:
1724
+ """Log specific reason why compliance settings are not available."""
1725
+ if not security_plan:
1726
+ logger.debug("Security plan not found")
1727
+ elif not hasattr(security_plan, "complianceSettingsId"):
1728
+ logger.debug("Security plan does not have complianceSettingsId attribute")
1729
+ elif not security_plan.complianceSettingsId:
1730
+ logger.debug("Security plan has no complianceSettingsId set")
1731
+
1732
+ def _get_implementation_status_from_result(self, result: str) -> str:
1733
+ """
1734
+ Get implementation status based on assessment result using compliance settings.
1735
+
1736
+ :param str result: Assessment result ('Pass' or 'Fail')
1737
+ :return: Implementation status string
1738
+ :rtype: str
1739
+ """
1740
+ logger.debug(f"Getting implementation status for result: {result}")
1741
+ compliance_settings = self._get_compliance_settings()
1742
+
1743
+ if compliance_settings:
1744
+ logger.debug(f"Using compliance settings: {compliance_settings.title}")
1745
+ try:
1746
+ status_labels = compliance_settings.get_field_labels("implementationStatus")
1747
+ logger.debug(f"Available status labels: {status_labels}")
1748
+
1749
+ best_match = self._find_best_status_match(result.lower(), status_labels)
1750
+ if best_match:
1751
+ return best_match
1752
+
1753
+ logger.debug(f"No matching compliance setting found for result: {result}")
1754
+ except Exception as e:
1755
+ logger.debug(f"Error using compliance settings for status mapping: {e}")
1756
+ else:
1757
+ logger.debug("No compliance settings available, using default mapping")
1758
+
1759
+ return self._get_default_status(result)
1760
+
1761
+ def _find_best_status_match(self, result_lower: str, status_labels: list) -> Optional[str]:
1762
+ """Find best matching status label for the given result."""
1763
+ best_match = None
1764
+ best_match_score = 0
1765
+
1766
+ for label in status_labels:
1767
+ label_lower = label.lower()
1768
+ logger.debug(f"Checking label '{label}' for result '{result_lower}'")
1769
+
1770
+ if result_lower == "pass":
1771
+ score = self._score_pass_result_label(label_lower)
1772
+ elif result_lower == "fail":
1773
+ score = self._score_fail_result_label(label_lower)
1774
+ else:
1775
+ score = 0
1776
+
1777
+ if score > best_match_score:
1778
+ best_match = label
1779
+ best_match_score = score
1780
+ logger.debug(f"New best match: '{label}' (score: {score})")
1781
+
1782
+ if best_match:
1783
+ logger.debug(
1784
+ f"Selected best match: '{best_match}' (final score: {best_match_score}) for {result_lower} result"
1785
+ )
1786
+
1787
+ return best_match
1788
+
1789
+ def _score_pass_result_label(self, label_lower: str) -> int:
1790
+ """Score a label for Pass results (higher score = better match)."""
1791
+ # Skip negative keywords that shouldn't match Pass results
1792
+ negative_keywords = ["not", "failed", "violation", "remediation", "unsatisfied", "non-compliant"]
1793
+ if any(neg_kw in label_lower for neg_kw in negative_keywords):
1794
+ return 0
1795
+
1796
+ # Exact word matches get highest priority
1797
+ exact_matches = {"implemented": 100, "complete": 95, "compliant": 90, "satisfied": 85}
1798
+ if label_lower in exact_matches:
1799
+ return exact_matches[label_lower]
1800
+
1801
+ # Partial matches get lower priority
1802
+ if "implemented" in label_lower and "fully" not in label_lower and "partially" not in label_lower:
1803
+ return 80
1804
+ elif "complete" in label_lower:
1805
+ return 75
1806
+ elif "compliant" in label_lower:
1807
+ return 70
1808
+ elif "satisfied" in label_lower:
1809
+ return 65
1810
+ elif "fully" in label_lower and "implemented" in label_lower:
1811
+ return 60
1812
+ elif "partially" in label_lower and "implemented" in label_lower:
1813
+ return 55
1814
+ elif "implemented" in label_lower:
1815
+ return 45
1816
+ elif any(kw in label_lower for kw in ["complete", "compliant", "satisfied"]):
1817
+ return 40
1818
+
1819
+ return 0
1820
+
1821
+ def _score_fail_result_label(self, label_lower: str) -> int:
1822
+ """Score a label for Fail results (higher score = better match)."""
1823
+ # Exact word matches get highest priority
1824
+ if label_lower in ["remediation", "in remediation"]:
1825
+ return 100
1826
+ elif label_lower in ["failed", "not implemented"]:
1827
+ return 95
1828
+ elif label_lower in ["non-compliant", "violation"]:
1829
+ return 90
1830
+ elif label_lower == "unsatisfied":
1831
+ return 85
1832
+
1833
+ # Partial matches get lower priority
1834
+ elif "remediation" in label_lower:
1835
+ return 80
1836
+ elif "failed" in label_lower or "not implemented" in label_lower:
1837
+ return 75
1838
+ elif any(kw in label_lower for kw in ["non-compliant", "violation", "unsatisfied"]):
1839
+ return 70
1840
+
1841
+ return 0
1842
+
1843
+ def _get_default_status(self, result: str) -> str:
1844
+ """Get default implementation status when no compliance settings are available."""
1845
+ default_status = "Fully Implemented" if result.lower() == "pass" else "In Remediation"
1846
+ logger.debug(f"Using default status: {default_status}")
1847
+ return default_status
1848
+
1625
1849
  def _update_implementation_status(self, implementation: ControlImplementation, result: str) -> None:
1626
1850
  """
1627
1851
  Update control implementation status based on assessment result.
1852
+ Uses compliance settings from the security plan if available, otherwise falls back to defaults.
1628
1853
 
1629
1854
  :param ControlImplementation implementation: Control implementation to update
1630
1855
  :param str result: Assessment result ('Pass' or 'Fail')
@@ -1632,10 +1857,8 @@ class ComplianceIntegration(ScannerIntegration, ABC):
1632
1857
  :rtype: None
1633
1858
  """
1634
1859
  try:
1635
- if result == "Pass":
1636
- new_status = "Fully Implemented"
1637
- else:
1638
- new_status = "In Remediation"
1860
+ # Get status from compliance settings or fallback to default
1861
+ new_status = self._get_implementation_status_from_result(result)
1639
1862
 
1640
1863
  # Update implementation status
1641
1864
  implementation.status = new_status
@@ -1653,7 +1876,7 @@ class ComplianceIntegration(ScannerIntegration, ABC):
1653
1876
  objective.status = new_status
1654
1877
  objective.save()
1655
1878
 
1656
- logger.debug(f"Updated implementation status to {new_status}")
1879
+ logger.debug(f"Updated implementation status to {new_status} (from compliance settings)")
1657
1880
 
1658
1881
  except Exception as e:
1659
1882
  logger.error(f"Error updating implementation status: {e}")
@@ -1851,6 +2074,8 @@ class ComplianceIntegration(ScannerIntegration, ABC):
1851
2074
  except Exception:
1852
2075
  pass
1853
2076
  existing_issue.dateLastUpdated = self.scan_date
2077
+ # Set organization ID based on Issue Owner or SSP Owner hierarchy
2078
+ existing_issue.orgId = self.determine_issue_organization_id(existing_issue.issueOwnerId)
1854
2079
  existing_issue.save()
1855
2080
 
1856
2081
  return existing_issue