Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions assets/parser_fixture_matrix_journalctl_short_full.log
Original file line number Diff line number Diff line change
Expand Up @@ -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]
1 change: 1 addition & 0 deletions assets/parser_fixture_matrix_syslog.log
Original file line number Diff line number Diff line change
Expand Up @@ -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]
2 changes: 1 addition & 1 deletion docs/parser-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
5 changes: 5 additions & 0 deletions src/parser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
38 changes: 30 additions & 8 deletions tests/test_parser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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",
Expand All @@ -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");
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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();
Expand Down
Loading