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 @@ -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]
Expand Down
1 change: 1 addition & 0 deletions assets/parser_fixture_matrix_syslog.log
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
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`. 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.

Expand Down
18 changes: 15 additions & 3 deletions src/parser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -303,19 +303,31 @@ 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);
if (username.empty()) {
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;
Expand Down
53 changes: 43 additions & 10 deletions tests/test_parser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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");
Expand All @@ -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");
Expand Down Expand Up @@ -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");
Expand All @@ -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();
Expand Down
Loading