Initial RPCScope release
This commit is contained in:
commit
9dc7289652
40 files changed
+6688
No files matched your search
@@ -0,0 +1,130 @@
|
||||
import contextlib
|
||||
import io
|
||||
import json
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from rpcscope.cli import main
|
||||
|
||||
|
||||
TOOLS = {"nxc": "nxc", "enum4linux-ng": None, "rpcdump": None, "msfconsole": None}
|
||||
|
||||
|
||||
class CliTests(unittest.TestCase):
|
||||
def test_fast_default_deep_collection_and_isolated_fallback_plans(self):
|
||||
for extra, deep, isolated in (([], False, False), (["--deep"], True, False),
|
||||
(["--isolated-checks"], False, True)):
|
||||
with self.subTest(extra=extra), tempfile.TemporaryDirectory() as temporary:
|
||||
captured = io.StringIO()
|
||||
destination = Path(temporary) / "absent"
|
||||
with patch("rpcscope.runner.doctor", return_value=TOOLS), contextlib.redirect_stdout(captured):
|
||||
code = main(["scan", "192.0.2.1", "--checks", "recommended", "--dry-run", "-o", str(destination), *extra])
|
||||
commands = json.loads(captured.getvalue())
|
||||
self.assertEqual(code, 0)
|
||||
self.assertFalse(destination.exists())
|
||||
self.assertEqual(any(command[0] == "enum4linux-ng" for command in commands), deep)
|
||||
self.assertEqual(sum("-M" in command for command in commands), 11 if isolated else 7)
|
||||
self.assertTrue(any(command[0] == "<native:longlogon>" for command in commands))
|
||||
|
||||
def test_tcp_share_dry_run_shows_gating_without_guessing_live_capabilities(self):
|
||||
captured = io.StringIO()
|
||||
with patch("rpcscope.runner.doctor", return_value=TOOLS), \
|
||||
patch("rpcscope.runner.inspect_capabilities") as inspect, \
|
||||
contextlib.redirect_stdout(captured):
|
||||
self.assertEqual(main(["scan", "192.0.2.0/24", "--shares", "--dry-run"]), 0)
|
||||
inspect.assert_not_called()
|
||||
commands = json.loads(captured.getvalue())
|
||||
self.assertEqual(commands[0][0], "<native:tcp-discovery>")
|
||||
self.assertEqual(commands[1][2], "<TCP/445-listeners>")
|
||||
self.assertIn("<version-specific-read-only-options>", commands[1])
|
||||
self.assertNotIn("--no-write-check", commands[1])
|
||||
|
||||
def test_invalid_limits_are_usage_errors(self):
|
||||
for argument in ("--jobs", "--discovery-workers", "--timeout", "--discovery-timeout"):
|
||||
with self.subTest(argument=argument), contextlib.redirect_stderr(io.StringIO()):
|
||||
with self.assertRaises(SystemExit) as raised:
|
||||
main(["scan", "192.0.2.1", argument, "0", "--dry-run"])
|
||||
self.assertEqual(raised.exception.code, 2)
|
||||
|
||||
def test_progress_and_final_reports_use_attributed_check_results(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
destination = Path(temporary) / "scan"
|
||||
captured, diagnostics = io.StringIO(), io.StringIO()
|
||||
|
||||
def execute(command, timeout, log, password, **kwargs):
|
||||
if "-M" not in command:
|
||||
return 0, "\n".join(
|
||||
f"SMB 192.0.2.{i} 445 HOST{i} [*] Windows 11 (name:HOST{i}) (domain:LAB) (signing:True) (SMBv1:False)"
|
||||
for i in (1, 2)
|
||||
)
|
||||
lines = []
|
||||
for host in command[2:command.index("-u=")]:
|
||||
lines.append(f"MS17-010 {host} 445 HOST1 {host} is likely VULNERABLE to MS17-010! (Windows 7)"
|
||||
if host.endswith(".1") else f"MS17-010 {host} 445 HOST2 {host} does NOT appear vulnerable")
|
||||
return 0, "\n".join(lines)
|
||||
|
||||
with patch("rpcscope.runner.doctor", return_value=TOOLS), patch("rpcscope.runner._run", side_effect=execute), \
|
||||
contextlib.redirect_stdout(captured), contextlib.redirect_stderr(diagnostics):
|
||||
code = main(["scan", "192.0.2.0/30", "--checks", "ms17-010", "--authorised",
|
||||
"--discovery", "smb", "--no-enum4linux", "--no-rpcdump", "--progress", "plain", "-o", str(destination)])
|
||||
self.assertEqual(code, 0)
|
||||
self.assertIn("[3/3] SUMMARY scan complete", diagnostics.getvalue())
|
||||
self.assertIn("VULN: [MS17-010]", captured.getvalue())
|
||||
self.assertIn("NOT VULNERABLE (SELECTED CHECKS)", captured.getvalue())
|
||||
report = json.loads((destination / "report.json").read_text())
|
||||
self.assertEqual(report["mode"], "live-scan")
|
||||
self.assertEqual(len(report["hosts"]), 2)
|
||||
self.assertEqual((destination / "report.txt").read_text(), captured.getvalue())
|
||||
self.assertIn("Host summary", (destination / "report.md").read_text())
|
||||
|
||||
def test_discovery_failure_returns_nonzero_but_retains_reports(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
destination = Path(temporary) / "scan"
|
||||
with patch("rpcscope.runner.doctor", return_value=TOOLS), patch("rpcscope.runner._run", return_value=(124, "")), \
|
||||
contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(io.StringIO()):
|
||||
code = main(["scan", "192.0.2.0/30", "--authorised", "--discovery", "smb", "--no-progress", "-o", str(destination)])
|
||||
self.assertEqual(code, 1)
|
||||
report = json.loads((destination / "report.json").read_text())
|
||||
self.assertTrue(report["errors"])
|
||||
self.assertEqual(report["hosts"], [])
|
||||
|
||||
def test_unknown_check_is_completed_with_zero_exit_not_a_negative(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
output = io.StringIO()
|
||||
diagnostics = io.StringIO()
|
||||
def execute(command, timeout, log, password, **kwargs):
|
||||
return (0, "") if "-M" in command else (0, "SMB 192.0.2.1 445 HOST [*] Windows 11 (name:HOST)")
|
||||
with patch("rpcscope.runner.doctor", return_value=TOOLS), patch("rpcscope.runner._run", side_effect=execute), \
|
||||
contextlib.redirect_stdout(output), contextlib.redirect_stderr(diagnostics):
|
||||
code = main(["scan", "192.0.2.1", "--checks", "smbghost", "--discovery", "smb", "--authorised",
|
||||
"--no-enum4linux", "--no-rpcdump", "-o", str(Path(temporary) / "out")])
|
||||
self.assertEqual(code, 0)
|
||||
self.assertIn("UNKNOWN:", output.getvalue())
|
||||
self.assertIn("CHECKS: 1/1", output.getvalue())
|
||||
self.assertNotIn("PARTIAL", output.getvalue())
|
||||
self.assertNotIn("NOT VULNERABLE", output.getvalue())
|
||||
self.assertIn("SUMMARY scan complete", diagnostics.getvalue())
|
||||
|
||||
def test_dry_run_has_no_files_and_keeps_json_on_stdout(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
destination = Path(temporary) / "scan"
|
||||
output = io.StringIO()
|
||||
with patch("rpcscope.runner.doctor", return_value=TOOLS), contextlib.redirect_stdout(output):
|
||||
code = main(["scan", "192.0.2.1", "--dry-run", "-o", str(destination)])
|
||||
self.assertEqual(code, 0)
|
||||
self.assertFalse(destination.exists())
|
||||
self.assertIsInstance(json.loads(output.getvalue()), list)
|
||||
|
||||
def test_missing_offline_input_is_reported_without_traceback(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
errors = io.StringIO()
|
||||
with contextlib.redirect_stderr(errors):
|
||||
code = main(["analyze", "--nxc", str(Path(temporary) / "missing.txt")])
|
||||
self.assertEqual(code, 1)
|
||||
self.assertIn("error:", errors.getvalue())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user