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.
- regscale/_version.py +1 -1
- regscale/integrations/commercial/jira.py +95 -22
- regscale/integrations/commercial/wizv2/click.py +23 -0
- regscale/integrations/commercial/wizv2/compliance_report.py +115 -26
- regscale/integrations/compliance_integration.py +230 -5
- regscale/models/integration_models/synqly_models/capabilities.json +1 -1
- regscale/models/regscale_models/control_implementation.py +13 -3
- {regscale_cli-6.24.0.0.dist-info → regscale_cli-6.24.0.1.dist-info}/METADATA +9 -9
- {regscale_cli-6.24.0.0.dist-info → regscale_cli-6.24.0.1.dist-info}/RECORD +13 -13
- {regscale_cli-6.24.0.0.dist-info → regscale_cli-6.24.0.1.dist-info}/LICENSE +0 -0
- {regscale_cli-6.24.0.0.dist-info → regscale_cli-6.24.0.1.dist-info}/WHEEL +0 -0
- {regscale_cli-6.24.0.0.dist-info → regscale_cli-6.24.0.1.dist-info}/entry_points.txt +0 -0
- {regscale_cli-6.24.0.0.dist-info → regscale_cli-6.24.0.1.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,10 +1857,8 @@ class ComplianceIntegration(ScannerIntegration, ABC):
|
|
|
1632
1857
|
:rtype: None
|
|
1633
1858
|
"""
|
|
1634
1859
|
try:
|
|
1635
|
-
|
|
1636
|
-
|
|
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
|