mirror of
https://github.com/hansjone/netx.git
synced 2026-10-08 22:20:58 +08:00
ZTE post-login banners can line-wrap login-failure stats and were misclassified before the target prompt was recognized. Co-authored-by: Cursor <cursoragent@cursor.com>
103 lines
4.5 KiB
Python
103 lines
4.5 KiB
Python
"""Unit tests for CLI auth-failure classification."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import unittest
|
|
|
|
from netx_api.ne_cli_errors import find_auth_failure_snippet, format_cli_failure
|
|
|
|
|
|
class CliAuthClassifyTests(unittest.TestCase):
|
|
def test_permission_denied_password(self):
|
|
text = "banner\nca-oper@114.1.105.3: Permission denied (password).\n"
|
|
self.assertIn("Permission denied", find_auth_failure_snippet(text) or "")
|
|
|
|
def test_prompt_timeout_promoted_to_auth(self):
|
|
exc = "ReadTimeout: Pattern not detected: '[>#]' in output."
|
|
transcript = "Warning\nca-oper@114.1.105.3: Permission denied (password).\n"
|
|
msg = format_cli_failure(exc, transcript)
|
|
self.assertTrue(msg.startswith("auth_rejected:"))
|
|
self.assertIn("Permission denied", msg)
|
|
self.assertIn("prompt_timeout", msg)
|
|
|
|
def test_plain_timeout_includes_session_log(self):
|
|
exc = RuntimeError("ReadTimeout: Pattern not detected: '[>#]' in output.")
|
|
msg = format_cli_failure(exc, "show running-config\n...still dumping...\n---- More ----\n")
|
|
self.assertIn("ReadTimeout", msg)
|
|
self.assertFalse(msg.startswith("auth_rejected:"))
|
|
self.assertIn("session log", msg)
|
|
self.assertIn("More", msg)
|
|
|
|
def test_authentication_exception(self):
|
|
class AuthenticationException(Exception):
|
|
pass
|
|
|
|
msg = format_cli_failure(AuthenticationException("target_auth_rejected: Permission denied"))
|
|
self.assertTrue(msg.startswith("auth_rejected:"))
|
|
|
|
def test_authentication_exception_empty_message(self):
|
|
class AuthenticationException(Exception):
|
|
pass
|
|
|
|
msg = format_cli_failure(AuthenticationException())
|
|
self.assertTrue(msg.startswith("auth_rejected:"))
|
|
|
|
def test_huawei_post_login_banner_not_auth_failure(self):
|
|
"""Bastion/SSH hop success banner must not be classified as auth reject."""
|
|
text = (
|
|
"Info: The max number of VTY users is 21, "
|
|
"the number of current VTY users online is 1.\n"
|
|
"The last successful login was performed at 19:28:16 08-02-2026 "
|
|
"from 10.229.147.122 through SSH. Afterwards, 0 authentication "
|
|
"failure occurred.\n"
|
|
"<HUAWEI>"
|
|
)
|
|
self.assertIsNone(find_auth_failure_snippet(text))
|
|
msg = format_cli_failure("ReadTimeout: Pattern not detected", text)
|
|
self.assertFalse(msg.startswith("auth_rejected:"))
|
|
# Still attach transcript for diagnostics (Huawei banner is not auth_rejected).
|
|
self.assertIn("session log", msg)
|
|
|
|
def test_real_auth_failure_still_detected_near_banner(self):
|
|
text = (
|
|
"The last successful login was performed at 19:28:16 08-02-2026 "
|
|
"from 10.229.147.122 through SSH. Afterwards, 0 authentication "
|
|
"failure occurred.\n"
|
|
"Error: Username or password is wrong.\n"
|
|
)
|
|
self.assertIn("Username or password is wrong", find_auth_failure_snippet(text) or "")
|
|
|
|
def test_zte_post_login_banner_not_auth_failure(self):
|
|
"""ZTE nested ssh hop: '0 authentication failure occurred' is login stats."""
|
|
text = (
|
|
"Welcome to ZXR10 ZXCTN 6120H Carrier-Class Router of ZTE Corporation\n"
|
|
"Login at 09:43:53 08-31-2026 from 10.229.147.122 through SSH.\n"
|
|
"The last successful login was performed at 09:43:44 08-31-2026 "
|
|
"from 10.229.147.122 through SSH. Afterwards, 0 authentication "
|
|
"failure occurred.\n"
|
|
"AL5458-ACC-6120HS#"
|
|
)
|
|
self.assertIsNone(find_auth_failure_snippet(text))
|
|
|
|
def test_zte_wrapped_afterwards_banner_not_auth_failure(self):
|
|
"""Narrow PTY wraps 'Afterwards' — must not classify as auth reject."""
|
|
text = (
|
|
"The last successful login was performed at 09:43:44 08-31-2026 "
|
|
"from 10.229.147.122 through SSH. After\n"
|
|
"wards, 0 authentication failure occurred.\n"
|
|
"AL5458-ACC-6120HS#"
|
|
)
|
|
self.assertIsNone(find_auth_failure_snippet(text))
|
|
|
|
def test_zte_severely_wrapped_afterwards_banner_not_auth_failure(self):
|
|
"""Production saw 'rwards' after mid-word wrap — still login stats, not reject."""
|
|
text = (
|
|
"from 10.229.147.122 through SSH. Afterwa\n"
|
|
"rwards, 0 authentication failure occurred.\n"
|
|
"AL5458-ACC-6120HS#"
|
|
)
|
|
self.assertIsNone(find_auth_failure_snippet(text))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|