Improve evidence classification and compact CLI reporting
This commit is contained in:
1 parent
9dc7289652
commit
dda4f8563f
28 files changed
+945
-87
No files matched your search
@@ -29,6 +29,64 @@ def alive(addresses, on_result, **kwargs):
|
||||
|
||||
|
||||
class BatchExecutionTests(unittest.TestCase):
|
||||
def test_rpcdump_exit_zero_requires_reliable_inventory_and_no_protocol_error(self):
|
||||
endpoint = "UUID: 12345678-1234-abcd-ef00-0123456789ab v1.0\nBindings:\n ncacn_ip_tcp:192.0.2.1[50000]\n"
|
||||
cases = [
|
||||
("[-] Protocol failed: access denied\n[*] No endpoints found.\n", "failed", True),
|
||||
("CRITICAL:root:Protocol failed: refused\n", "failed", True),
|
||||
("", "failed", True),
|
||||
("[*] Received one endpoint.\n", "failed", True),
|
||||
(endpoint + "[-] Protocol failed: incomplete lookup\n", "failed", True),
|
||||
("[*] No endpoints found.\n", "success", False),
|
||||
(endpoint + "Provider: ModuleNotFoundError: remote annotation\n", "success", False),
|
||||
]
|
||||
for text, status, has_errors in cases:
|
||||
with self.subTest(text=text), tempfile.TemporaryDirectory() as temporary:
|
||||
config = ScanConfig(["192.0.2.1"], out_dir=Path(temporary), use_enum4linux=False, use_rpcdump=True)
|
||||
def execute(command, timeout, log, password, **kwargs):
|
||||
return (0, text) if command[0] == "fake-rpc" else (0, banner("192.0.2.1"))
|
||||
with patch("rpcscope.runner.doctor", return_value=TOOLS), patch("rpcscope.runner._run", side_effect=execute):
|
||||
host = scan(config)["192.0.2.1"]
|
||||
self.assertEqual(host.collection["rpcdump"], status)
|
||||
self.assertEqual(bool(config.errors), has_errors)
|
||||
self.assertEqual(coverage(host)[2], has_errors)
|
||||
if endpoint in text:
|
||||
self.assertTrue(host.rpc_interfaces) # keep partial evidence
|
||||
|
||||
def test_local_rpcdump_dependency_error_stops_queued_copies_not_other_checks(self):
|
||||
addresses = ["192.0.2.1", "192.0.2.2", "192.0.2.3"]
|
||||
calls = []
|
||||
def execute(command, timeout, log, password, **kwargs):
|
||||
calls.append(command)
|
||||
if command[0] == "fake-rpc":
|
||||
return 1, "Traceback (most recent call last):\nModuleNotFoundError: No module named 'impacket'"
|
||||
if "-M" in command:
|
||||
return 0, f"MS17-010 {command[2]} 445 HOST {command[2]} does NOT appear vulnerable"
|
||||
return 0, "\n".join(banner(address) for address in addresses)
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
config = ScanConfig(addresses, out_dir=Path(temporary), jobs=1, use_enum4linux=False,
|
||||
use_rpcdump=True, checks=resolve_checks(["ms17-010"]))
|
||||
with patch("rpcscope.runner.doctor", return_value=TOOLS), patch("rpcscope.runner._run", side_effect=execute):
|
||||
hosts = scan(config)
|
||||
self.assertEqual(sum(command[0] == "fake-rpc" for command in calls), 1)
|
||||
self.assertEqual(sum("-M" in command for command in calls), 3)
|
||||
self.assertEqual(hosts[addresses[0]].collection["rpcdump"], "failed")
|
||||
self.assertEqual(hosts[addresses[1]].collection["rpcdump"], "skipped-local-error")
|
||||
self.assertTrue(all(host.checks["ms17-010"] == "not-vulnerable" for host in hosts.values()))
|
||||
self.assertIn("ModuleNotFoundError", config.errors[0])
|
||||
self.assertTrue(any(item["stage"] == "rpcdump" and item["exit_code"] == 1 for item in config.timings))
|
||||
|
||||
def test_remote_rpcdump_failure_does_not_disable_other_targets(self):
|
||||
addresses = ["192.0.2.1", "192.0.2.2"]
|
||||
def execute(command, timeout, log, password, **kwargs):
|
||||
return (1, "RPC access denied") if command[0] == "fake-rpc" else (0, "\n".join(banner(address) for address in addresses))
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
config = ScanConfig(addresses, out_dir=Path(temporary), jobs=1, use_enum4linux=False, use_rpcdump=True)
|
||||
with patch("rpcscope.runner.doctor", return_value=TOOLS), patch("rpcscope.runner._run", side_effect=execute):
|
||||
hosts = scan(config)
|
||||
self.assertTrue(all(host.collection["rpcdump"] == "failed" for host in hosts.values()))
|
||||
self.assertEqual(len(config.errors), 2)
|
||||
|
||||
def test_duplicate_api_checks_are_executed_and_counted_once(self):
|
||||
definition = resolve_checks(["ms17-010"])[0]
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
@@ -84,7 +142,7 @@ class BatchExecutionTests(unittest.TestCase):
|
||||
self.assertEqual(hosts["192.0.2.1"].checks, {})
|
||||
self.assertTrue(any("failed locally" in message for message in config.errors))
|
||||
|
||||
def test_recommended_32_host_scan_uses_28_processes_with_full_attribution(self):
|
||||
def test_recommended_32_host_scan_uses_24_processes_with_full_attribution(self):
|
||||
addresses = [f"192.0.2.{i}" for i in range(1, 33)]
|
||||
commands = []
|
||||
def execute(command, timeout, log, password, **kwargs):
|
||||
@@ -118,11 +176,12 @@ class BatchExecutionTests(unittest.TestCase):
|
||||
patch("rpcscope.runner.inspect_capabilities", return_value=CURRENT), \
|
||||
patch("rpcscope.runner.discover_services", side_effect=alive), \
|
||||
patch("rpcscope.runner._run", side_effect=execute), \
|
||||
patch("rpcscope.runner.probe_longlogon", return_value=CheckResult("precondition-not-met", "not patch detection")):
|
||||
patch("rpcscope.runner.probe_longlogon", return_value=CheckResult("precondition-not-met", "not patch detection")), \
|
||||
patch("rpcscope.runner.probe_smb_compression", return_value=CheckResult("exposed", "compression capability only")):
|
||||
hosts = scan(config, progress=events.append, checkpoint=lambda value: snapshots.append(len(value)))
|
||||
check_commands = [command for command in commands if "-M" in command]
|
||||
self.assertEqual(len(check_commands), 28)
|
||||
self.assertEqual(len(commands), 29) # one separate inventory invocation
|
||||
self.assertEqual(len(check_commands), 24)
|
||||
self.assertEqual(len(commands), 25) # one separate inventory invocation
|
||||
self.assertEqual(len(hosts), 32)
|
||||
self.assertGreater(len(snapshots), 1)
|
||||
for host in hosts.values():
|
||||
@@ -130,7 +189,7 @@ class BatchExecutionTests(unittest.TestCase):
|
||||
self.assertEqual(coverage(host), (12, 12, False))
|
||||
self.assertEqual(host.checks["zerologon"], "inconclusive")
|
||||
self.assertEqual(host.checks["ms17-010"], "not-vulnerable")
|
||||
self.assertEqual(host.checks["smbghost"], "suspected")
|
||||
self.assertEqual(host.checks["smbghost"], "exposed")
|
||||
self.assertEqual(host.checks["oxid"], "observed")
|
||||
self.assertTrue(all(host.checks[key] == "exposed" for key in EXPOSURE_CHECKS))
|
||||
self.assertEqual(config.errors, [])
|
||||
|
||||
Reference in new issue
Block a user