Files
rpcscope/tests/test_progress.py

166 lines
7.2 KiB
Python

import io
import unittest
from unittest.mock import patch
from rpcscope.progress import ProgressEvent, ProgressReporter
class Clock:
def __init__(self):
self.now = 0.0
def __call__(self):
return self.now
class Terminal(io.StringIO):
def isatty(self):
return True
class ProgressTests(unittest.TestCase):
def test_redirected_output_has_results_and_no_terminal_codes(self):
output = io.StringIO()
clock = Clock()
reporter = ProgressReporter(output, clock=clock)
reporter(ProgressEvent("start", "ms17-010", "10.0.0.1", total=2))
clock.now = 1.25
reporter(ProgressEvent("finish", "ms17-010", "10.0.0.1", "vulnerable", 1, 2))
reporter(ProgressEvent("summary", "scan", status="complete", completed=2, total=2))
self.assertIn("[0/2] START ms17-010 10.0.0.1", output.getvalue())
self.assertIn("[1/2] DONE ms17-010 10.0.0.1 vulnerable (1.2s)", output.getvalue())
self.assertIn("[2/2] SUMMARY scan complete", output.getvalue())
self.assertNotIn("\x1b", output.getvalue())
self.assertNotIn("\r", output.getvalue())
def test_heartbeat_requires_inactivity_and_an_active_task(self):
output = io.StringIO()
clock = Clock()
reporter = ProgressReporter(output, clock=clock, interval=10)
reporter._render()
self.assertEqual(output.getvalue(), "")
reporter(ProgressEvent("start", "discovery", total=3))
initial = output.getvalue()
clock.now = 9
reporter._render()
self.assertEqual(output.getvalue(), initial)
clock.now = 10
reporter._render()
self.assertIn("[0/3] RUNNING discovery (elapsed 10s)", output.getvalue())
first_heartbeat = output.getvalue()
clock.now = 19
reporter._render()
self.assertEqual(output.getvalue(), first_heartbeat)
reporter(ProgressEvent("summary", "scan", status="complete", completed=3, total=3))
final = output.getvalue()
clock.now = 99
reporter._render()
self.assertEqual(output.getvalue(), final)
def test_parallel_tasks_and_out_of_order_counts(self):
output = io.StringIO()
clock = Clock()
reporter = ProgressReporter(output, clock=clock)
reporter(ProgressEvent("start", "rpcdump", "10.0.0.1", total=4))
reporter(ProgressEvent("start", "rpcdump", "10.0.0.2", total=4))
clock.now = 10
reporter._render()
self.assertIn("10.0.0.1 (+1 active)", output.getvalue())
reporter(ProgressEvent("finish", "rpcdump", "10.0.0.2", "failed", 2, 4))
reporter(ProgressEvent("finish", "rpcdump", "10.0.0.1", "ok", 1, 4))
self.assertIn("[2/4] DONE rpcdump 10.0.0.1 ok", output.getvalue())
def test_silent_discovery_advance_keeps_counts_without_printing_dead_hosts(self):
output = io.StringIO()
clock = Clock()
reporter = ProgressReporter(output, clock=clock, interval=10)
reporter(ProgressEvent("start", "tcp-discovery", total=254))
initial = output.getvalue()
clock.now = 9
reporter(ProgressEvent("advance", "tcp-discovery", "192.0.2.9", "closed", 9, 254))
self.assertEqual(output.getvalue(), initial)
clock.now = 10
reporter._render()
self.assertIn("[9/254] RUNNING tcp-discovery (elapsed 10s)", output.getvalue())
self.assertNotIn("192.0.2.9", output.getvalue())
reporter(ProgressEvent("finish", "tcp-discovery", "192.0.2.10", "alive", 10, 254))
self.assertIn("[10/254] DONE tcp-discovery 192.0.2.10 alive", output.getvalue())
def test_tty_advance_does_not_draw_dead_target_or_keep_it_active(self):
output = Terminal()
reporter = ProgressReporter(output)
reporter(ProgressEvent("start", "tcp-discovery", total=254))
initial = output.getvalue()
reporter(ProgressEvent("advance", "tcp-discovery", "192.0.2.9", "timeout", 1, 254))
self.assertEqual(output.getvalue(), initial)
self.assertNotIn("192.0.2.9", reporter._dashboard(reporter.clock()))
self.assertIn("[1/254]", reporter._dashboard(reporter.clock()))
def test_hostless_discovery_advance_preserves_aggregate_heartbeat(self):
output = io.StringIO()
clock = Clock()
reporter = ProgressReporter(output, clock=clock, interval=10)
reporter(ProgressEvent("start", "tcp-discovery", total=254))
initial = output.getvalue()
reporter(ProgressEvent("advance", "tcp-discovery", completed=1, total=254))
self.assertEqual(output.getvalue(), initial)
clock.now = 10
reporter._render()
self.assertIn("[1/254] RUNNING tcp-discovery (elapsed 10s)", output.getvalue())
def test_untrusted_text_cannot_inject_control_sequences_or_lines(self):
output = io.StringIO()
reporter = ProgressReporter(output, mode="plain")
reporter(ProgressEvent(
"finish", "check", "10.0.0.1\r\nFAKE", "\x1b[31mvulnerable\x1b[0m",
detail="one\tline\x00\u202e\nonly",
))
self.assertEqual(len(output.getvalue().splitlines()), 1)
self.assertIn("10.0.0.1 FAKE vulnerable", output.getvalue())
self.assertIn("one line only", output.getvalue())
self.assertNotIn("\x1b", output.getvalue())
self.assertNotIn("\u202e", output.getvalue())
def test_tty_dashboard_retains_completed_result(self):
output = Terminal()
clock = Clock()
reporter = ProgressReporter(output, clock=clock, color="never")
reporter(ProgressEvent("start", "check", "10.0.0.1", total=1))
self.assertIn("\r\x1b[2K[0/1] RUNNING check 10.0.0.1", output.getvalue())
reporter(ProgressEvent("finish", "check", "10.0.0.1", "inconclusive", 1, 1))
self.assertIn("DONE check 10.0.0.1 inconclusive (0.0s)\n", output.getvalue())
reporter.__exit__(None, None, None)
self.assertTrue(output.getvalue().endswith("\n"))
def test_plain_mode_never_uses_ansi_even_on_a_terminal(self):
output = Terminal()
reporter = ProgressReporter(output, mode="plain")
reporter(ProgressEvent("start", "discovery"))
self.assertNotIn("\x1b", output.getvalue())
def test_context_starts_and_joins_heartbeat_thread(self):
output = io.StringIO()
with patch("rpcscope.progress.threading.Thread") as thread:
with ProgressReporter(output) as reporter:
reporter(ProgressEvent("start", "discovery"))
thread.return_value.start.assert_called_once()
thread.return_value.join.assert_called_once()
self.assertTrue(reporter._stop.is_set())
final = output.getvalue()
reporter(ProgressEvent("start", "ignored"))
reporter._render()
self.assertEqual(output.getvalue(), final)
def test_quiet_does_not_create_a_thread_or_output(self):
output = io.StringIO()
with patch("rpcscope.progress.threading.Thread") as thread:
with ProgressReporter(output, mode="quiet") as reporter:
reporter(ProgressEvent("start", "discovery"))
reporter._render()
thread.assert_not_called()
self.assertEqual(output.getvalue(), "")
if __name__ == "__main__":
unittest.main()