regscale-cli 6.24.0.0__py3-none-any.whl → 6.25.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 (32) hide show
  1. regscale/_version.py +1 -1
  2. regscale/core/app/api.py +1 -1
  3. regscale/core/app/application.py +5 -3
  4. regscale/core/app/internal/evidence.py +308 -202
  5. regscale/dev/code_gen.py +84 -3
  6. regscale/integrations/commercial/__init__.py +2 -0
  7. regscale/integrations/commercial/jira.py +95 -22
  8. regscale/integrations/commercial/microsoft_defender/defender.py +326 -5
  9. regscale/integrations/commercial/microsoft_defender/defender_api.py +348 -14
  10. regscale/integrations/commercial/microsoft_defender/defender_constants.py +157 -0
  11. regscale/integrations/commercial/synqly/assets.py +99 -16
  12. regscale/integrations/commercial/synqly/query_builder.py +533 -0
  13. regscale/integrations/commercial/synqly/vulnerabilities.py +134 -14
  14. regscale/integrations/commercial/wizv2/click.py +23 -0
  15. regscale/integrations/commercial/wizv2/compliance_report.py +137 -26
  16. regscale/integrations/compliance_integration.py +247 -5
  17. regscale/integrations/scanner_integration.py +16 -0
  18. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  19. regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +12 -2
  20. regscale/models/integration_models/synqly_models/filter_parser.py +332 -0
  21. regscale/models/integration_models/synqly_models/synqly_model.py +47 -3
  22. regscale/models/regscale_models/compliance_settings.py +28 -0
  23. regscale/models/regscale_models/component.py +1 -0
  24. regscale/models/regscale_models/control_implementation.py +143 -4
  25. regscale/regscale.py +1 -1
  26. regscale/validation/record.py +23 -1
  27. {regscale_cli-6.24.0.0.dist-info → regscale_cli-6.25.0.0.dist-info}/METADATA +9 -9
  28. {regscale_cli-6.24.0.0.dist-info → regscale_cli-6.25.0.0.dist-info}/RECORD +32 -30
  29. {regscale_cli-6.24.0.0.dist-info → regscale_cli-6.25.0.0.dist-info}/LICENSE +0 -0
  30. {regscale_cli-6.24.0.0.dist-info → regscale_cli-6.25.0.0.dist-info}/WHEEL +0 -0
  31. {regscale_cli-6.24.0.0.dist-info → regscale_cli-6.25.0.0.dist-info}/entry_points.txt +0 -0
  32. {regscale_cli-6.24.0.0.dist-info → regscale_cli-6.25.0.0.dist-info}/top_level.txt +0 -0
@@ -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,15 +1857,30 @@ 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
1642
1865
  implementation.dateLastAssessed = get_current_datetime()
1643
1866
  implementation.lastAssessmentResult = result
1867
+
1868
+ # Ensure required fields are set if empty
1869
+ if not implementation.responsibility:
1870
+ implementation.responsibility = ControlImplementation.get_default_responsibility(
1871
+ parent_id=implementation.parentId
1872
+ )
1873
+ logger.debug(
1874
+ f"Setting default responsibility for implementation {implementation.id}: {implementation.responsibility}"
1875
+ )
1876
+
1877
+ if not implementation.implementation:
1878
+ control_id = (
1879
+ getattr(implementation.control, "controlId", "control") if implementation.control else "control"
1880
+ )
1881
+ implementation.implementation = f"Implementation details for {control_id} will be documented."
1882
+ logger.debug(f"Setting default implementation statement for implementation {implementation.id}")
1883
+
1644
1884
  implementation.save()
1645
1885
 
1646
1886
  # Update objectives if they exist
@@ -1653,7 +1893,7 @@ class ComplianceIntegration(ScannerIntegration, ABC):
1653
1893
  objective.status = new_status
1654
1894
  objective.save()
1655
1895
 
1656
- logger.debug(f"Updated implementation status to {new_status}")
1896
+ logger.debug(f"Updated implementation status to {new_status} (from compliance settings)")
1657
1897
 
1658
1898
  except Exception as e:
1659
1899
  logger.error(f"Error updating implementation status: {e}")
@@ -1851,6 +2091,8 @@ class ComplianceIntegration(ScannerIntegration, ABC):
1851
2091
  except Exception:
1852
2092
  pass
1853
2093
  existing_issue.dateLastUpdated = self.scan_date
2094
+ # Set organization ID based on Issue Owner or SSP Owner hierarchy
2095
+ existing_issue.orgId = self.determine_issue_organization_id(existing_issue.issueOwnerId)
1854
2096
  existing_issue.save()
1855
2097
 
1856
2098
  return existing_issue
@@ -1162,6 +1162,21 @@ class ScannerIntegration(ABC):
1162
1162
  self.components_by_title[component_name] = component
1163
1163
  return component
1164
1164
 
1165
+ def _get_compliance_settings_id(self) -> Optional[int]:
1166
+ """
1167
+ Get the compliance settings ID from the security plan.
1168
+
1169
+ :return: The compliance settings ID if available
1170
+ :rtype: Optional[int]
1171
+ """
1172
+ try:
1173
+ security_plan = regscale_models.SecurityPlan.get_object(object_id=self.plan_id)
1174
+ if security_plan and hasattr(security_plan, "complianceSettingsId"):
1175
+ return security_plan.complianceSettingsId
1176
+ except Exception as e:
1177
+ logger.debug(f"Failed to get compliance settings ID from security plan {self.plan_id}: {e}")
1178
+ return None
1179
+
1165
1180
  def _create_new_component(self, asset: IntegrationAsset, component_name: str) -> regscale_models.Component:
1166
1181
  """
1167
1182
  Create a new component for the asset.
@@ -1178,6 +1193,7 @@ class ScannerIntegration(ABC):
1178
1193
  securityPlansId=self.plan_id,
1179
1194
  description=component_name,
1180
1195
  componentOwnerId=self.get_assessor_id(),
1196
+ complianceSettingsId=self._get_compliance_settings_id(),
1181
1197
  ).get_or_create()
1182
1198
  self.components.append(component)
1183
1199
  return component