From 60186a12e3cf3648974038eee6fb4e00634bd55d Mon Sep 17 00:00:00 2001 From: oliver Date: Sun, 2 Aug 2026 20:12:03 +0800 Subject: [PATCH] Ignore Huawei/ZTE post-login banners when classifying auth failures. Bastion hop success notices mention authentication failure count and were misread as target_auth_rejected. Co-authored-by: Cursor --- netx_api/ne_cli_errors.py | 14 +++++++++++++- tests/test_ne_cli_errors.py | 23 +++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/netx_api/ne_cli_errors.py b/netx_api/ne_cli_errors.py index dacc7af..ab321ff 100644 --- a/netx_api/ne_cli_errors.py +++ b/netx_api/ne_cli_errors.py @@ -26,12 +26,24 @@ _AUTH_PATTERNS: tuple[re.Pattern[str], ...] = tuple( ) ) +# Huawei / ZTE post-login security banner (success path via SSH or bastion hop). +# Example: "Afterwards, 0 authentication failure occurred." +_LOGIN_SUCCESS_NOTICE = re.compile( + r"^.*(?:" + r"last\s+successful\s+login\s+was\s+performed" + r"|afterwards,\s*\d+\s+authentication\s+failures?\s+occurred" + r"|上次成功登录" + r"|之后发生了?\s*\d+\s*次认证失败" + r").*$", + re.I | re.M, +) + _PROMPT_TIMEOUT = re.compile(r"pattern not detected|readtimeout|read timeout", re.I) def find_auth_failure_snippet(text: str, *, max_len: int = 220) -> str | None: """Return a short matching auth-failure line/snippet, or None.""" - blob = str(text or "") + blob = _LOGIN_SUCCESS_NOTICE.sub("", str(text or "")) if not blob.strip(): return None for pat in _AUTH_PATTERNS: diff --git a/tests/test_ne_cli_errors.py b/tests/test_ne_cli_errors.py index cbe7c6e..c308284 100644 --- a/tests/test_ne_cli_errors.py +++ b/tests/test_ne_cli_errors.py @@ -40,6 +40,29 @@ class CliAuthClassifyTests(unittest.TestCase): 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" + "" + ) + self.assertIsNone(find_auth_failure_snippet(text)) + msg = format_cli_failure("ReadTimeout: Pattern not detected", text) + self.assertFalse(msg.startswith("auth_rejected:")) + + 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 "") + if __name__ == "__main__": unittest.main()