From a6d1e1285b8b1c1d315b5620d20a44e2810d1684 Mon Sep 17 00:00:00 2001 From: stacknil Date: Sat, 6 Jun 2026 13:22:52 +0800 Subject: [PATCH] test(parser): normalize max-auth error prefix --- ...r_fixture_matrix_journalctl_short_full.log | 1 + assets/parser_fixture_matrix_syslog.log | 1 + docs/parser-contract.md | 2 +- src/parser.cpp | 5 +++ tests/test_parser.cpp | 38 +++++++++++++++---- 5 files changed, 38 insertions(+), 9 deletions(-) diff --git a/assets/parser_fixture_matrix_journalctl_short_full.log b/assets/parser_fixture_matrix_journalctl_short_full.log index d7193ac..c007c83 100644 --- a/assets/parser_fixture_matrix_journalctl_short_full.log +++ b/assets/parser_fixture_matrix_journalctl_short_full.log @@ -25,3 +25,4 @@ Tue 2026-03-10 09:05:34 UTC example-host sshd[3007]: Timeout, client not respond Tue 2026-03-10 09:05:46 UTC example-host sshd[3010]: Received disconnect from 203.0.113.55 port 52015:11: disconnected by user Tue 2026-03-10 09:05:58 UTC example-host sshd[3011]: Unable to negotiate with 203.0.113.56 port 52016: no matching host key type found. Their offer: ssh-rsa Tue 2026-03-10 09:06:10 UTC example-host pam_unix(sshd:session): session closed for user alice +Tue 2026-03-10 09:06:24 UTC example-host sshd[3023]: error: maximum authentication attempts exceeded for invalid user svc-error-maxauth from 203.0.113.57 port 52019 ssh2 [preauth] diff --git a/assets/parser_fixture_matrix_syslog.log b/assets/parser_fixture_matrix_syslog.log index 2707bd7..9cfe3da 100644 --- a/assets/parser_fixture_matrix_syslog.log +++ b/assets/parser_fixture_matrix_syslog.log @@ -25,3 +25,4 @@ Mar 10 09:05:34 example-host sshd[2007]: Timeout, client not responding from 203 Mar 10 09:05:46 example-host sshd[2010]: Received disconnect from 203.0.113.55 port 52015:11: disconnected by user Mar 10 09:05:58 example-host sshd[2011]: Unable to negotiate with 203.0.113.56 port 52016: no matching host key type found. Their offer: ssh-rsa Mar 10 09:06:10 example-host pam_unix(sshd:session): session closed for user alice +Mar 10 09:06:24 example-host sshd[2023]: error: maximum authentication attempts exceeded for invalid user svc-error-maxauth from 203.0.113.57 port 52019 ssh2 [preauth] diff --git a/docs/parser-contract.md b/docs/parser-contract.md index aac905e..13e7ee8 100644 --- a/docs/parser-contract.md +++ b/docs/parser-contract.md @@ -26,7 +26,7 @@ The parser currently recognizes common authentication evidence from: - selected `pam_faillock(...)` variants - selected `pam_sss(...)` variants -Recognized SSH failure families include failed password, invalid user, illegal user, failed publickey, failed keyboard-interactive/pam, and maximum-authentication-attempts-exceeded lines. `illegal user` is treated as an OpenSSH wording variant of `invalid user`. Invalid or illegal-user variants of keyboard-interactive and maximum-authentication-attempts-exceeded lines are normalized into `ssh_invalid_user` events. Recognized SSH failures can become detection signals through the configured signal mapping. +Recognized SSH failure families include failed password, invalid user, illegal user, failed publickey, failed keyboard-interactive/pam, and maximum-authentication-attempts-exceeded lines. `illegal user` is treated as an OpenSSH wording variant of `invalid user`. Maximum-authentication-attempts lines may include OpenSSH's leading `error:` marker and still normalize into the same event family. Invalid or illegal-user variants of keyboard-interactive and maximum-authentication-attempts-exceeded lines are normalized into `ssh_invalid_user` events. Recognized SSH failures can become detection signals through the configured signal mapping. Recognized success or audit families include accepted password, accepted publickey, accepted keyboard-interactive/pam, sudo command audit lines, sudo password failures, sudoers policy denials, su success/failure audit lines, and selected PAM session/auth lines. diff --git a/src/parser.cpp b/src/parser.cpp index 3689c51..8a55b71 100644 --- a/src/parser.cpp +++ b/src/parser.cpp @@ -418,6 +418,11 @@ bool parse_ssh_failed_keyboard_interactive_message(std::string_view message, Eve bool parse_ssh_max_auth_tries_message(std::string_view message, Event& event) { static constexpr std::string_view max_auth_prefix = "maximum authentication attempts exceeded for "; + static constexpr std::string_view error_prefix = "error: "; + if (message.starts_with(error_prefix)) { + message.remove_prefix(error_prefix.size()); + } + if (!message.starts_with(max_auth_prefix)) { return false; } diff --git a/tests/test_parser.cpp b/tests/test_parser.cpp index f248338..b7aa91a 100644 --- a/tests/test_parser.cpp +++ b/tests/test_parser.cpp @@ -291,6 +291,19 @@ void test_max_auth_tries_event() { "expected ssh max-auth-tries failure type"); } +void test_max_auth_tries_error_prefix_event() { + const auto parser = make_syslog_parser(); + const auto event = parser.parse_line( + "Mar 10 08:27:25 example-host sshd[1251]: error: maximum authentication attempts exceeded for frank from 203.0.113.84 port 51248 ssh2 [preauth]", + 5); + + expect(event.has_value(), "expected error-prefixed max-auth-tries event"); + expect(event->username == "frank", "expected parsed error-prefixed max-auth-tries username"); + expect(event->source_ip == "203.0.113.84", "expected parsed error-prefixed max-auth-tries source ip"); + expect(event->event_type == loglens::EventType::SshMaxAuthTries, + "expected error-prefixed ssh max-auth-tries failure type"); +} + void test_max_auth_tries_invalid_user_event() { const auto parser = make_syslog_parser(); const auto event = parser.parse_line( @@ -632,12 +645,12 @@ void test_syslog_fixture_matrix_file() { const auto parser = make_syslog_parser(); const auto result = parser.parse_file(asset_path("parser_fixture_matrix_syslog.log")); - expect(result.events.size() == 19, "expected nineteen recognized syslog fixture events"); + expect(result.events.size() == 20, "expected twenty recognized syslog fixture events"); expect(result.warnings.size() == 8, "expected eight syslog fixture warnings"); - expect(result.quality.total_lines == 27, "expected twenty-seven syslog fixture lines"); - expect(result.quality.parsed_lines == 19, "expected nineteen parsed syslog fixture lines"); + expect(result.quality.total_lines == 28, "expected twenty-eight syslog fixture lines"); + expect(result.quality.parsed_lines == 20, "expected twenty parsed syslog fixture lines"); expect(result.quality.unparsed_lines == 8, "expected eight unparsed syslog fixture lines"); - expect_close(result.quality.parse_success_rate, 19.0 / 27.0, 1e-9, "expected syslog fixture parse success rate"); + expect_close(result.quality.parse_success_rate, 20.0 / 28.0, 1e-9, "expected syslog fixture parse success rate"); expect(result.events[0].event_type == loglens::EventType::SshInvalidUser, "expected invalid-user failed password"); expect(result.events[1].event_type == loglens::EventType::SshFailedPublicKey, "expected failed publickey variant"); @@ -684,6 +697,10 @@ void test_syslog_fixture_matrix_file() { expect(result.events[18].event_type == loglens::EventType::SshInvalidUser, "expected direct illegal-user variant"); expect(result.events[18].username == "legacy-backup", "expected direct illegal username"); + expect(result.events[19].event_type == loglens::EventType::SshInvalidUser, + "expected error-prefixed max-auth-tries invalid-user variant"); + expect(result.events[19].username == "svc-error-maxauth", + "expected error-prefixed max-auth-tries invalid username"); expect(result.quality.top_unknown_patterns.size() == 4, "expected four unknown syslog buckets"); expect(result.quality.top_unknown_patterns[0].pattern == "sshd_connection_closed_preauth", @@ -706,12 +723,12 @@ void test_journalctl_fixture_matrix_file() { std::nullopt}); const auto result = parser.parse_file(asset_path("parser_fixture_matrix_journalctl_short_full.log")); - expect(result.events.size() == 19, "expected nineteen recognized journalctl fixture events"); + expect(result.events.size() == 20, "expected twenty recognized journalctl fixture events"); expect(result.warnings.size() == 8, "expected eight journalctl fixture warnings"); - expect(result.quality.total_lines == 27, "expected twenty-seven journalctl fixture lines"); - expect(result.quality.parsed_lines == 19, "expected nineteen parsed journalctl fixture lines"); + expect(result.quality.total_lines == 28, "expected twenty-eight journalctl fixture lines"); + expect(result.quality.parsed_lines == 20, "expected twenty parsed journalctl fixture lines"); expect(result.quality.unparsed_lines == 8, "expected eight unparsed journalctl fixture lines"); - expect_close(result.quality.parse_success_rate, 19.0 / 27.0, 1e-9, "expected journalctl fixture parse success rate"); + expect_close(result.quality.parse_success_rate, 20.0 / 28.0, 1e-9, "expected journalctl fixture parse success rate"); expect(result.events[0].event_type == loglens::EventType::SshInvalidUser, "expected journalctl invalid-user failed password"); expect(result.events[1].event_type == loglens::EventType::SshFailedPublicKey, "expected journalctl failed publickey variant"); @@ -748,6 +765,10 @@ void test_journalctl_fixture_matrix_file() { expect(result.events[18].event_type == loglens::EventType::SshInvalidUser, "expected journalctl direct illegal-user variant"); expect(result.events[18].username == "legacy-backup", "expected journalctl direct illegal username"); + expect(result.events[19].event_type == loglens::EventType::SshInvalidUser, + "expected journalctl error-prefixed max-auth-tries invalid-user variant"); + expect(result.events[19].username == "svc-error-maxauth", + "expected journalctl error-prefixed max-auth-tries invalid username"); expect(result.quality.top_unknown_patterns.size() == 4, "expected four unknown journalctl buckets"); expect(result.quality.top_unknown_patterns[0].pattern == "sshd_connection_closed_preauth", @@ -785,6 +806,7 @@ int main() { test_failed_keyboard_interactive_invalid_user_event(); test_failed_keyboard_interactive_illegal_user_event(); test_max_auth_tries_event(); + test_max_auth_tries_error_prefix_event(); test_max_auth_tries_invalid_user_event(); test_max_auth_tries_illegal_user_event(); test_pam_auth_failure_event();