@@ -36,6 +36,13 @@ const loglens::AuthSignal* find_signal(const std::vector<loglens::AuthSignal>& s
3636 return it == signals.end () ? nullptr : &(*it);
3737}
3838
39+ std::size_t count_signals (const std::vector<loglens::AuthSignal>& signals,
40+ loglens::AuthSignalKind signal_kind) {
41+ return static_cast <std::size_t >(std::count_if (signals.begin (), signals.end (), [&](const loglens::AuthSignal& signal) {
42+ return signal.signal_kind == signal_kind;
43+ }));
44+ }
45+
3946std::vector<loglens::Event> parse_events (loglens::ParserConfig config, std::string_view input_text) {
4047 const loglens::AuthLogParser parser (config);
4148 std::istringstream input (std::string{input_text});
@@ -87,6 +94,23 @@ std::vector<loglens::Event> build_pam_bruteforce_candidate_events() {
8794 " Mar 10 08:18:05 example-host pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=203.0.113.10 user=root\n " );
8895}
8996
97+ std::vector<loglens::Event> build_sudo_signal_candidate_events () {
98+ return parse_events (
99+ make_syslog_config (),
100+ " Mar 10 08:21:00 example-host sudo: alice : TTY=pts/0 ; PWD=/home/alice ; USER=root ; COMMAND=/usr/bin/systemctl restart ssh\n "
101+ " Mar 10 08:21:05 example-host pam_unix(sudo:session): session opened for user root by alice(uid=0)\n "
102+ " Mar 10 08:21:10 example-host pam_unix(sshd:session): session closed for user alice\n " );
103+ }
104+
105+ std::vector<loglens::Event> build_sudo_burst_preservation_events () {
106+ return parse_events (
107+ make_syslog_config (),
108+ " Mar 10 08:21:00 example-host sudo: alice : TTY=pts/0 ; PWD=/home/alice ; USER=root ; COMMAND=/usr/bin/systemctl restart ssh\n "
109+ " Mar 10 08:21:05 example-host pam_unix(sudo:session): session opened for user root by alice(uid=0)\n "
110+ " Mar 10 08:22:10 example-host sudo: alice : TTY=pts/0 ; PWD=/home/alice ; USER=root ; COMMAND=/usr/bin/journalctl -xe\n "
111+ " Mar 10 08:24:15 example-host sudo: alice : TTY=pts/0 ; PWD=/home/alice ; USER=root ; COMMAND=/usr/bin/vi /etc/ssh/sshd_config\n " );
112+ }
113+
90114void test_default_thresholds () {
91115 const auto events = build_events ();
92116 const loglens::Detector detector;
@@ -149,6 +173,61 @@ void test_failed_publickey_contributes_to_bruteforce_by_default() {
149173 expect (brute_force->event_count == 5 , " expected publickey evidence to raise brute force count to five" );
150174}
151175
176+ void test_sudo_signals_include_command_and_session_opened () {
177+ const auto events = build_sudo_signal_candidate_events ();
178+ const auto signals = loglens::build_auth_signals (events, loglens::DetectorConfig{}.auth_signal_mappings );
179+
180+ expect (signals.size () == 2 , " expected sudo command and supported sudo session-opened signals only" );
181+ expect (count_signals (signals, loglens::AuthSignalKind::SudoCommand) == 1 ,
182+ " expected one sudo command signal" );
183+ expect (count_signals (signals, loglens::AuthSignalKind::SudoSessionOpened) == 1 ,
184+ " expected one sudo session-opened signal" );
185+
186+ const auto * sudo_command = find_signal (signals, loglens::AuthSignalKind::SudoCommand);
187+ expect (sudo_command != nullptr , " expected sudo command signal" );
188+ expect (sudo_command->counts_as_sudo_burst_evidence ,
189+ " expected sudo command signal to count toward sudo burst evidence" );
190+ expect (!sudo_command->counts_as_attempt_evidence , " did not expect sudo command to count as auth attempt evidence" );
191+ expect (!sudo_command->counts_as_terminal_auth_failure ,
192+ " did not expect sudo command to count as terminal auth failure" );
193+
194+ const auto * sudo_session = find_signal (signals, loglens::AuthSignalKind::SudoSessionOpened);
195+ expect (sudo_session != nullptr , " expected sudo session-opened signal" );
196+ expect (!sudo_session->counts_as_sudo_burst_evidence ,
197+ " expected sudo session-opened signal to stay out of sudo burst counting by default" );
198+ expect (!sudo_session->counts_as_attempt_evidence ,
199+ " did not expect sudo session-opened to count as auth attempt evidence" );
200+ expect (!sudo_session->counts_as_terminal_auth_failure ,
201+ " did not expect sudo session-opened to count as terminal auth failure" );
202+ }
203+
204+ void test_sudo_burst_behavior_is_preserved_with_signal_layer () {
205+ const auto events = build_sudo_burst_preservation_events ();
206+ const loglens::Detector detector;
207+ const auto findings = detector.analyze (events);
208+
209+ const auto * sudo = find_finding (findings, loglens::FindingType::SudoBurst, " alice" );
210+ expect (sudo != nullptr , " expected sudo burst finding" );
211+ expect (sudo->event_count == 3 ,
212+ " expected sudo burst count to remain based on command events rather than session-opened lines" );
213+ }
214+
215+ void test_unsupported_pam_session_close_remains_telemetry_only () {
216+ const loglens::AuthLogParser parser (make_syslog_config ());
217+ std::istringstream input (
218+ " Mar 10 09:06:10 example-host pam_unix(sudo:session): session closed for user alice\n " );
219+
220+ const auto result = parser.parse_stream (input);
221+ expect (result.events .empty (), " expected unsupported session-close line to stay out of parsed events" );
222+ expect (result.warnings .size () == 1 , " expected unsupported session-close line to produce one warning" );
223+ expect (result.quality .top_unknown_patterns .size () == 1 , " expected one unknown pattern bucket" );
224+ expect (result.quality .top_unknown_patterns .front ().pattern == " pam_unix_other" ,
225+ " expected unsupported session-close line to remain in pam_unix_other telemetry" );
226+
227+ const auto signals = loglens::build_auth_signals (result.events , loglens::DetectorConfig{}.auth_signal_mappings );
228+ expect (signals.empty (), " expected unsupported session-close line to stay out of the signal layer" );
229+ }
230+
152231void test_pam_auth_failure_does_not_trigger_bruteforce_by_default () {
153232 const auto events = build_pam_bruteforce_candidate_events ();
154233 const loglens::Detector detector;
@@ -264,6 +343,9 @@ int main() {
264343 test_custom_thresholds ();
265344 test_auth_signal_defaults ();
266345 test_failed_publickey_contributes_to_bruteforce_by_default ();
346+ test_sudo_signals_include_command_and_session_opened ();
347+ test_sudo_burst_behavior_is_preserved_with_signal_layer ();
348+ test_unsupported_pam_session_close_remains_telemetry_only ();
267349 test_pam_auth_failure_does_not_trigger_bruteforce_by_default ();
268350 test_equivalent_attack_scenario_yields_same_finding_count_across_modes ();
269351 test_load_valid_config ();
0 commit comments