113 lines
5.5 KiB
Python
113 lines
5.5 KiB
Python
import unittest
|
|
|
|
from rpcscope.models import HostEvidence, RpcInterface, Share
|
|
from rpcscope.rules import evaluate_host
|
|
|
|
|
|
class RuleTests(unittest.TestCase):
|
|
def test_negative_capability_with_retained_conflict_still_gets_review_finding(self):
|
|
host = HostEvidence("192.0.2.1", checks={"smbghost": "exposed"})
|
|
host.merge(HostEvidence(host.address, checks={"smbghost": "not-exposed"}))
|
|
findings = evaluate_host(host)
|
|
self.assertEqual(len(findings), 1)
|
|
self.assertIn("conflict", findings[0].title)
|
|
self.assertEqual(findings[0].confidence, "low")
|
|
|
|
def test_rules_separate_exposure_from_confirmed_vulnerability(self):
|
|
host = HostEvidence(
|
|
address="10.10.10.25",
|
|
smb1=True,
|
|
signing=False,
|
|
null_session=True,
|
|
shares=[Share("Public", ["READ", "WRITE"])],
|
|
rpc_interfaces=[RpcInterface("12345678-1234-abcd-ef00-0123456789ab", "1.0")],
|
|
)
|
|
findings = evaluate_host(host)
|
|
ids = {finding.rule_id for finding in findings}
|
|
self.assertTrue({"SMB-001", "SMB-002", "SMB-003", "SMB-005"} <= ids)
|
|
rpc = next(finding for finding in findings if finding.rule_id.startswith("RPC-"))
|
|
self.assertIn("does not prove", rpc.impact)
|
|
self.assertNotIn("MSF-MS17-010", ids)
|
|
|
|
|
|
def test_msf_result_is_explicit(self):
|
|
host = HostEvidence(address="10.10.10.25", checks={"ms17-010": "vulnerable"})
|
|
self.assertTrue(any(f.rule_id == "CHECK-MS17-010" for f in evaluate_host(host)))
|
|
|
|
def test_catalog_check_becomes_detailed_finding(self):
|
|
host = HostEvidence(address="10.10.10.25", checks={"zerologon": "vulnerable"})
|
|
finding = next(f for f in evaluate_host(host) if f.rule_id == "CHECK-ZEROLOGON")
|
|
self.assertEqual(finding.severity, "critical")
|
|
self.assertIn("CVE-2020-1472", finding.title)
|
|
|
|
def test_rpc_registration_does_not_claim_remote_reachability(self):
|
|
host = HostEvidence("10.10.10.25", rpc_interfaces=[
|
|
RpcInterface("12345678-1234-abcd-ef00-0123456789ab", "1.0",
|
|
endpoint="ncalrpc:[local-spooler]")
|
|
])
|
|
finding = evaluate_host(host)[0]
|
|
self.assertEqual(finding.severity, "info")
|
|
self.assertIn("local bindings only", finding.title)
|
|
self.assertIn("reachability untested", finding.evidence)
|
|
|
|
def test_rpc_report_preserves_remote_binding_after_local_binding(self):
|
|
host = HostEvidence("10.10.10.25", rpc_interfaces=[
|
|
RpcInterface("12345678-1234-abcd-ef00-0123456789ab", "1.0",
|
|
bindings=["ncalrpc:[local-spooler]", "ncacn_ip_tcp:10.10.10.25[49665]"])
|
|
])
|
|
finding = evaluate_host(host)[0]
|
|
self.assertEqual(finding.severity, "medium")
|
|
self.assertIn("ncacn_ip_tcp", finding.evidence)
|
|
self.assertNotIn("local bindings only", finding.title)
|
|
|
|
def test_legacy_fingerprint_uses_version_boundaries(self):
|
|
for os in ("Windows 11", "Windows Server 2019", "Windows 80", "Samba 4.20.1"):
|
|
with self.subTest(os=os):
|
|
self.assertFalse(any(f.rule_id == "SMB-007" for f in evaluate_host(
|
|
HostEvidence("10.10.10.25", os=os)
|
|
)))
|
|
for os in ("Windows 7 Build 7601", "Windows 8.1", "Windows Server 2008 R2", "Samba 3.6.1"):
|
|
with self.subTest(os=os):
|
|
self.assertTrue(any(f.rule_id == "SMB-007" for f in evaluate_host(
|
|
HostEvidence("10.10.10.25", os=os)
|
|
)))
|
|
|
|
def test_acl_write_capability_does_not_claim_direct_write(self):
|
|
host = HostEvidence("10.10.10.25", shares=[Share("Data", ["READ", "WRITE (ACL)"])])
|
|
finding = evaluate_host(host)[0]
|
|
self.assertEqual(finding.rule_id, "SMB-005")
|
|
self.assertIn("permission-control", finding.title)
|
|
self.assertIn("WRITE (ACL)", finding.evidence)
|
|
|
|
def test_suspected_checker_indicator_is_preserved_with_low_confidence(self):
|
|
host = HostEvidence("10.10.10.25", checks={"smbghost": "suspected"},
|
|
check_details={"smbghost": "SMB 3.1.1 compression capability observed"})
|
|
finding = evaluate_host(host)[0]
|
|
self.assertEqual(finding.confidence, "low")
|
|
self.assertIn("requires validation", finding.title)
|
|
self.assertEqual(finding.evidence, host.check_details["smbghost"])
|
|
self.assertIn("never SMBGhost patch state", finding.impact)
|
|
|
|
def test_checker_positive_includes_actual_evidence(self):
|
|
host = HostEvidence("10.10.10.25", checks={"ms17-010": "vulnerable"},
|
|
check_details={"ms17-010": "positive checker marker from raw artifact"})
|
|
finding = evaluate_host(host)[0]
|
|
self.assertEqual(finding.confidence, "high")
|
|
self.assertEqual(finding.evidence, host.check_details["ms17-010"])
|
|
|
|
def test_rpc_evidence_preserves_version_to_binding_mapping(self):
|
|
host = HostEvidence("10.10.10.25", rpc_interfaces=[
|
|
RpcInterface("12345678-1234-abcd-ef00-0123456789ab", "1.0",
|
|
endpoint="ncalrpc:[local-spooler]"),
|
|
RpcInterface("12345678-1234-abcd-ef00-0123456789ab", "2.0",
|
|
endpoint="ncacn_ip_tcp:10.10.10.25[49665]"),
|
|
])
|
|
finding = evaluate_host(host)[0]
|
|
self.assertIn("v1.0 advertised via ncalrpc:[local-spooler]", finding.evidence)
|
|
self.assertIn("v2.0 advertised via ncacn_ip_tcp:10.10.10.25[49665]", finding.evidence)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|
|
|