From d673d949452a28ea0cb4c885ae7f3921e8881c4a Mon Sep 17 00:00:00 2001 From: stacknil Date: Sun, 7 Jun 2026 02:22:02 +0800 Subject: [PATCH] test(parser): normalize failed-none invalid users --- ...r_fixture_matrix_journalctl_short_full.log | 1 + assets/parser_fixture_matrix_syslog.log | 1 + docs/parser-contract.md | 2 +- src/parser.cpp | 18 +++++-- tests/test_parser.cpp | 53 +++++++++++++++---- 5 files changed, 61 insertions(+), 14 deletions(-) diff --git a/assets/parser_fixture_matrix_journalctl_short_full.log b/assets/parser_fixture_matrix_journalctl_short_full.log index 4c1670d..c6ffa6d 100644 --- a/assets/parser_fixture_matrix_journalctl_short_full.log +++ b/assets/parser_fixture_matrix_journalctl_short_full.log @@ -17,6 +17,7 @@ Tue 2026-03-10 09:03:39 UTC example-host sshd[3019]: Failed keyboard-interactive Tue 2026-03-10 09:03:39 UTC example-host sshd[3020]: maximum authentication attempts exceeded for invalid user svc-maxauth from 203.0.113.47 port 52009 ssh2 [preauth] Tue 2026-03-10 09:03:39 UTC example-host sshd[3021]: Failed password for illegal user legacy-admin from 203.0.113.48 port 52017 ssh2 Tue 2026-03-10 09:03:39 UTC example-host sshd[3022]: Illegal user legacy-backup from 203.0.113.49 port 52018 +Tue 2026-03-10 09:03:39 UTC example-host sshd[3025]: Failed none for invalid user svc-none from 203.0.113.59 port 52021 ssh2 Tue 2026-03-10 09:03:40 UTC example-host sshd[3003]: Connection closed by user alice 203.0.113.50 port 52010 [preauth] Tue 2026-03-10 09:04:05 UTC example-host sshd[3004]: Connection closed by authenticating user carol 203.0.113.51 port 52011 [preauth] Tue 2026-03-10 09:04:28 UTC example-host sshd[3005]: Connection closed by invalid user deploy 203.0.113.52 port 52012 [preauth] diff --git a/assets/parser_fixture_matrix_syslog.log b/assets/parser_fixture_matrix_syslog.log index 254de92..935f458 100644 --- a/assets/parser_fixture_matrix_syslog.log +++ b/assets/parser_fixture_matrix_syslog.log @@ -17,6 +17,7 @@ Mar 10 09:03:39 example-host sshd[2019]: Failed keyboard-interactive/pam for inv Mar 10 09:03:39 example-host sshd[2020]: maximum authentication attempts exceeded for invalid user svc-maxauth from 203.0.113.47 port 52009 ssh2 [preauth] Mar 10 09:03:39 example-host sshd[2021]: Failed password for illegal user legacy-admin from 203.0.113.48 port 52017 ssh2 Mar 10 09:03:39 example-host sshd[2022]: Illegal user legacy-backup from 203.0.113.49 port 52018 +Mar 10 09:03:39 example-host sshd[2025]: Failed none for invalid user svc-none from 203.0.113.59 port 52021 ssh2 Mar 10 09:03:40 example-host sshd[2003]: Connection closed by user alice 203.0.113.50 port 52010 [preauth] Mar 10 09:04:05 example-host sshd[2004]: Connection closed by authenticating user carol 203.0.113.51 port 52011 [preauth] Mar 10 09:04:28 example-host sshd[2005]: Connection closed by invalid user deploy 203.0.113.52 port 52012 [preauth] diff --git a/docs/parser-contract.md b/docs/parser-contract.md index 13e7ee8..71989b5 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`. 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 SSH failure families include failed password, invalid user, illegal user, failed publickey, failed keyboard-interactive/pam, failed-none invalid-user probing, 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 failed-none probing, 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 cf61a25..6ab9652 100644 --- a/src/parser.cpp +++ b/src/parser.cpp @@ -303,12 +303,20 @@ bool consume_invalid_or_illegal_user_prefix(std::string_view& remaining) { } bool parse_ssh_failed_message(std::string_view message, Event& event) { - static constexpr std::string_view failed_prefix = "Failed password for "; - if (!message.starts_with(failed_prefix)) { + static constexpr std::string_view failed_password_prefix = "Failed password for "; + static constexpr std::string_view failed_none_prefix = "Failed none for "; + + bool failed_none = false; + std::string_view remaining; + if (message.starts_with(failed_password_prefix)) { + remaining = message.substr(failed_password_prefix.size()); + } else if (message.starts_with(failed_none_prefix)) { + failed_none = true; + remaining = message.substr(failed_none_prefix.size()); + } else { return false; } - auto remaining = message.substr(failed_prefix.size()); const bool invalid_user = consume_invalid_or_illegal_user_prefix(remaining); const auto username = consume_token(remaining); @@ -316,6 +324,10 @@ bool parse_ssh_failed_message(std::string_view message, Event& event) { return false; } + if (failed_none && !invalid_user) { + return false; + } + event.username.assign(username); event.source_ip = extract_token_after(message, " from "); event.event_type = invalid_user ? EventType::SshInvalidUser : EventType::SshFailedPassword; diff --git a/tests/test_parser.cpp b/tests/test_parser.cpp index f6decfa..1895465 100644 --- a/tests/test_parser.cpp +++ b/tests/test_parser.cpp @@ -91,6 +91,31 @@ void test_illegal_user_failure_is_normalized_as_invalid_user() { "expected illegal-user failed-password to normalize to invalid-user type"); } +void test_failed_none_invalid_user_is_normalized_as_invalid_user() { + const auto parser = make_syslog_parser(); + const auto event = parser.parse_line( + "Mar 10 08:11:25 example-host sshd[1237]: Failed none for invalid user svc-none from 203.0.113.13 port 51025 ssh2", + 1); + + expect(event.has_value(), "expected failed-none invalid-user event"); + expect(event->username == "svc-none", "expected failed-none invalid username"); + expect(event->source_ip == "203.0.113.13", "expected failed-none invalid source ip"); + expect(event->event_type == loglens::EventType::SshInvalidUser, + "expected failed-none invalid-user to normalize to invalid-user type"); +} + +void test_failed_none_without_invalid_user_stays_unsupported() { + const auto parser = make_syslog_parser(); + std::string error; + const auto event = parser.parse_line( + "Mar 10 08:11:26 example-host sshd[1238]: Failed none for root from 203.0.113.14 port 51026 ssh2", + 1, + &error); + + expect(!event.has_value(), "expected failed-none standard user to stay unsupported"); + expect(error == "unrecognized auth pattern: sshd_other", "expected failed-none standard user telemetry bucket"); +} + void test_illegal_user_message_is_normalized_as_invalid_user() { const auto parser = make_syslog_parser(); const auto event = parser.parse_line( @@ -646,12 +671,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() == 20, "expected twenty recognized syslog fixture events"); + expect(result.events.size() == 21, "expected twenty-one recognized syslog fixture events"); expect(result.warnings.size() == 9, "expected nine syslog fixture warnings"); - expect(result.quality.total_lines == 29, "expected twenty-nine syslog fixture lines"); - expect(result.quality.parsed_lines == 20, "expected twenty parsed syslog fixture lines"); + expect(result.quality.total_lines == 30, "expected thirty syslog fixture lines"); + expect(result.quality.parsed_lines == 21, "expected twenty-one parsed syslog fixture lines"); expect(result.quality.unparsed_lines == 9, "expected nine unparsed syslog fixture lines"); - expect_close(result.quality.parse_success_rate, 20.0 / 29.0, 1e-9, "expected syslog fixture parse success rate"); + expect_close(result.quality.parse_success_rate, 21.0 / 30.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"); @@ -699,8 +724,11 @@ void test_syslog_fixture_matrix_file() { "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 failed-none invalid-user variant"); + expect(result.events[19].username == "svc-none", "expected failed-none invalid username"); + expect(result.events[20].event_type == loglens::EventType::SshInvalidUser, "expected error-prefixed max-auth-tries invalid-user variant"); - expect(result.events[19].username == "svc-error-maxauth", + expect(result.events[20].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"); @@ -724,12 +752,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() == 20, "expected twenty recognized journalctl fixture events"); + expect(result.events.size() == 21, "expected twenty-one recognized journalctl fixture events"); expect(result.warnings.size() == 9, "expected nine journalctl fixture warnings"); - expect(result.quality.total_lines == 29, "expected twenty-nine journalctl fixture lines"); - expect(result.quality.parsed_lines == 20, "expected twenty parsed journalctl fixture lines"); + expect(result.quality.total_lines == 30, "expected thirty journalctl fixture lines"); + expect(result.quality.parsed_lines == 21, "expected twenty-one parsed journalctl fixture lines"); expect(result.quality.unparsed_lines == 9, "expected nine unparsed journalctl fixture lines"); - expect_close(result.quality.parse_success_rate, 20.0 / 29.0, 1e-9, "expected journalctl fixture parse success rate"); + expect_close(result.quality.parse_success_rate, 21.0 / 30.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"); @@ -767,8 +795,11 @@ void test_journalctl_fixture_matrix_file() { "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 failed-none invalid-user variant"); + expect(result.events[19].username == "svc-none", "expected journalctl failed-none invalid username"); + expect(result.events[20].event_type == loglens::EventType::SshInvalidUser, "expected journalctl error-prefixed max-auth-tries invalid-user variant"); - expect(result.events[19].username == "svc-error-maxauth", + expect(result.events[20].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"); @@ -791,6 +822,8 @@ void test_journalctl_fixture_matrix_file() { int main() { test_invalid_user_failure(); test_illegal_user_failure_is_normalized_as_invalid_user(); + test_failed_none_invalid_user_is_normalized_as_invalid_user(); + test_failed_none_without_invalid_user_stays_unsupported(); test_illegal_user_message_is_normalized_as_invalid_user(); test_standard_failure(); test_success_event();