"""Regression cases based on upstream checker output, without network access.""" import unittest from rpcscope.checks import classify_msf, classify_nxc HOST = "10.20.30.41" OTHER_HOST = "10.20.30.42" def nxc_line(module, message, host=HOST): return f"{module:16} {host} 445 HOST1 {message}\n" class CheckerOutputTests(unittest.TestCase): def test_ms17_indeterminate_message_is_never_positive(self): result = classify_nxc( "ms17-010", HOST, nxc_line("MS17-010", f"[-] {HOST} Unable to detect if this host is vulnerable"), ) self.assertEqual(result.status, "inconclusive") def test_ms17_upstream_positive_and_negative_markers(self): cases = [ (f"[+] {HOST} is likely VULNERABLE to MS17-010! (Windows 7)", "vulnerable"), ("[+] VULNERABLE", "vulnerable"), (f"[-] {HOST} does NOT appear vulnerable", "not-vulnerable"), ] for message, expected in cases: with self.subTest(message=message): self.assertEqual( classify_nxc("ms17-010", HOST, nxc_line("MS17-010", message)).status, expected, ) def test_markers_must_match_target_and_module(self): text = ( nxc_line("MS17-010", "[+] VULNERABLE", OTHER_HOST) + nxc_line("ZEROLOGON", "[+] VULNERABLE") + nxc_line("SMB", r"[+] LAB\auditor:VULNERABLE") + "description: target may be VULNERABLE\n" ) self.assertEqual(classify_nxc("ms17-010", HOST, text).status, "inconclusive") def test_ansi_and_truncated_module_name_are_attributed(self): text = "\x1b[32m" + nxc_line( "PRINTNIGH...", "[+] Vulnerable, next step https://github.com/ly4k/PrintNightmare", ) + "\x1b[0m" self.assertEqual(classify_nxc("printnightmare", HOST, text).status, "suspected") def test_smbghost_capability_indicator_is_only_suspected(self): text = nxc_line( "SMBGHOST", "[+] Potentially vulnerable to SMBGhost (CVE-2020-0796)", ) self.assertEqual(classify_nxc("smbghost", HOST, text).status, "suspected") def test_nonzero_exit_fails_closed_even_with_result_markers(self): for message in ("[+] VULNERABLE", f"[-] {HOST} does NOT appear vulnerable"): with self.subTest(message=message): result = classify_nxc( "ms17-010", HOST, nxc_line("MS17-010", message), returncode=124, ) self.assertEqual(result.status, "inconclusive") self.assertIn("124", result.evidence) self.assertEqual(classify_msf( HOST, f"[+] {HOST}:445 - Host is likely VULNERABLE to MS17-010!\n", 1, ).status, "inconclusive") def test_conflicting_nxc_markers_require_review(self): text = ( nxc_line("MS17-010", "[+] VULNERABLE") + nxc_line("MS17-010", f"[-] {HOST} does NOT appear vulnerable") ) result = classify_nxc("ms17-010", HOST, text) self.assertEqual(result.status, "inconclusive") self.assertIn("conflicting", result.evidence) def test_silence_and_upstream_errors_remain_inconclusive(self): cases = [ ("ms17-010", "MS17-010", ""), ("zerologon", "ZEROLOGON", "[+] Attack failed. Target is probably patched."), ("printnightmare", "PRINTNIGHTMARE", "[-] Failed to bind: access denied"), ("smbghost", "SMBGHOST", "[-] Error while connecting to host: timed out"), ] for key, module, message in cases: with self.subTest(key=key): text = nxc_line(module, message) if message else "" self.assertEqual(classify_nxc(key, HOST, text).status, "inconclusive") def test_removed_module_is_explicitly_unsupported(self): text = nxc_line( "NTLM_REFLECTION", "[-] [REMOVED] This module has been integrated into the enum_cve module.", ) self.assertEqual(classify_nxc("ntlm-reflection", HOST, text).status, "skipped-unsupported") def test_msf_results_require_target_and_conflicts_are_inconclusive(self): positive = f"[+] {HOST}:445 - Host is likely VULNERABLE to MS17-010! (Windows 7)\n" negative = f"[-] {HOST}:445 - Host does NOT appear vulnerable.\n" self.assertEqual(classify_msf(HOST, positive).status, "vulnerable") self.assertEqual(classify_msf(HOST, negative).status, "not-vulnerable") self.assertEqual(classify_msf(HOST, positive.replace(HOST, OTHER_HOST)).status, "inconclusive") result = classify_msf(HOST, positive + negative) self.assertEqual(result.status, "inconclusive") self.assertIn("conflicting", result.evidence) if __name__ == "__main__": unittest.main()