From a4b6aaa8989a3855afabdc016aa35ca35748e286 Mon Sep 17 00:00:00 2001 From: stacknil Date: Sun, 12 Jul 2026 14:42:40 +0800 Subject: [PATCH] feat(signal): expose privilege auth audit signals --- src/signal.cpp | 21 ++++++++++++++++++--- src/signal.hpp | 5 ++++- tests/test_detector.cpp | 40 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 4 deletions(-) diff --git a/src/signal.cpp b/src/signal.cpp index 4841f99..6a4c349 100644 --- a/src/signal.cpp +++ b/src/signal.cpp @@ -69,13 +69,28 @@ std::optional signal_mapping_for_event(const Event& event, const false}; } return std::nullopt; + case EventType::SudoAuthFailure: + return SignalMapping{ + AuthSignalKind::SudoAuthFailure, + false, + false, + false}; + case EventType::SudoPolicyDenied: + return SignalMapping{ + AuthSignalKind::SudoPolicyDenied, + false, + false, + false}; + case EventType::SuAuthFailure: + return SignalMapping{ + AuthSignalKind::SuAuthFailure, + false, + false, + false}; case EventType::Unknown: case EventType::SshAcceptedPassword: case EventType::SshAcceptedPublicKey: case EventType::SshAcceptedKeyboardInteractive: - case EventType::SudoAuthFailure: - case EventType::SudoPolicyDenied: - case EventType::SuAuthFailure: default: return std::nullopt; } diff --git a/src/signal.hpp b/src/signal.hpp index 004a6e6..1533568 100644 --- a/src/signal.hpp +++ b/src/signal.hpp @@ -17,7 +17,10 @@ enum class AuthSignalKind { SshMaxAuthTries, PamAuthFailure, SudoCommand, - SudoSessionOpened + SudoSessionOpened, + SudoAuthFailure, + SudoPolicyDenied, + SuAuthFailure }; struct AuthSignalBehavior { diff --git a/tests/test_detector.cpp b/tests/test_detector.cpp index 200b0ad..3331657 100644 --- a/tests/test_detector.cpp +++ b/tests/test_detector.cpp @@ -570,6 +570,45 @@ void test_sudo_signals_include_command_and_session_opened() { "did not expect sudo session-opened to count as terminal auth failure"); } +void test_privilege_auth_failures_are_non_counting_signals() { + const auto events = parse_events( + make_syslog_config(), + "Mar 10 08:21:40 example-host sudo: alice : 1 incorrect password attempt ; TTY=pts/0 ; " + "PWD=/home/user/project ; USER=root ; COMMAND=/usr/bin/id\n" + "Mar 10 08:21:45 example-host sudo: bob : user NOT in sudoers ; TTY=pts/1 ; " + "PWD=/home/user/project ; USER=root ; COMMAND=/usr/bin/id\n" + "Mar 10 08:21:50 example-host su[1243]: FAILED SU (to root) carol on pts/2\n"); + expect(events.size() == 3, "expected sudo and su auth failures to parse"); + expect(events[0].event_type == loglens::EventType::SudoAuthFailure, + "expected sudo password failure event"); + expect(events[1].event_type == loglens::EventType::SudoPolicyDenied, + "expected sudo policy denied event"); + expect(events[2].event_type == loglens::EventType::SuAuthFailure, + "expected su auth failure event"); + + const auto signals = loglens::build_auth_signals(events, loglens::DetectorConfig{}.auth_signal_mappings); + expect(signals.size() == 3, "expected privilege auth failures to be visible as signals"); + expect(count_signals(signals, loglens::AuthSignalKind::SudoAuthFailure) == 1, + "expected one sudo auth failure signal"); + expect(count_signals(signals, loglens::AuthSignalKind::SudoPolicyDenied) == 1, + "expected one sudo policy denied signal"); + expect(count_signals(signals, loglens::AuthSignalKind::SuAuthFailure) == 1, + "expected one su auth failure signal"); + + for (const auto& signal : signals) { + expect(!signal.counts_as_attempt_evidence, + "did not expect privilege auth signals to count as auth attempt evidence"); + expect(!signal.counts_as_terminal_auth_failure, + "did not expect privilege auth signals to count as terminal auth failures"); + expect(!signal.counts_as_sudo_burst_evidence, + "did not expect privilege auth signals to count toward sudo burst evidence"); + } + + const loglens::Detector detector; + const auto findings = detector.analyze(events); + expect(findings.empty(), "expected privilege auth signals to stay out of detector findings by default"); +} + void test_sudo_burst_behavior_is_preserved_with_signal_layer() { const auto events = build_sudo_burst_preservation_events(); const loglens::Detector detector; @@ -743,6 +782,7 @@ int main() { test_failed_publickey_contributes_to_bruteforce_by_default(); test_accepted_publickey_success_stays_out_of_failure_signals(); test_sudo_signals_include_command_and_session_opened(); + test_privilege_auth_failures_are_non_counting_signals(); test_sudo_burst_behavior_is_preserved_with_signal_layer(); test_unsupported_pam_session_close_remains_telemetry_only(); test_pam_auth_failure_does_not_trigger_bruteforce_by_default();