From 1dd308d8a0cc8fffd74b2f461cff881607392593 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 29 Sep 2026 07:26:58 +0000 Subject: [PATCH] test: IPv6 SSH samples and detlab.scan CLI coverage - ssh_failed_password: an IPv6 source must match - ssh_bruteforce: a 6-in-25s burst from 2001:db8::7 must alert, grouped by the IPv6 address like any IPv4 source - tests for `python -m detlab.scan` (file, stdin, --year, skipped-line count and exit codes); scan.py coverage 71% -> 98% The IPv6 cases also run through the Wazuh logtest workflow, which checks that Wazuh's built-in sshd decoder extracts IPv6 srcip for the correlation. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FQvnacbQuWZv9PUmgednWt --- tests/samples/linux/ssh_bruteforce.yml | 10 ++++++ tests/samples/linux/ssh_failed_password.yml | 3 ++ tests/test_scan_cli.py | 36 +++++++++++++++++++++ 3 files changed, 49 insertions(+) create mode 100644 tests/test_scan_cli.py diff --git a/tests/samples/linux/ssh_bruteforce.yml b/tests/samples/linux/ssh_bruteforce.yml index 43503ed..4f9234b 100644 --- a/tests/samples/linux/ssh_bruteforce.yml +++ b/tests/samples/linux/ssh_bruteforce.yml @@ -11,6 +11,16 @@ cases: - 'Sep 28 10:00:15 web1 sshd[811]: Failed password for root from 203.0.113.7 port 50004 ssh2' - 'Sep 28 10:00:20 web1 sshd[811]: Failed password for root from 203.0.113.7 port 50005 ssh2' - 'Sep 28 10:00:25 web1 sshd[811]: Failed password for root from 203.0.113.7 port 50006 ssh2' + - description: 6 failures from one IPv6 source in 25 seconds (IPv6 is grouped like IPv4) + expect: alert + groups: [["2001:db8::7"]] + raw: + - 'Sep 28 10:00:00 web1 sshd[811]: Failed password for root from 2001:db8::7 port 50010 ssh2' + - 'Sep 28 10:00:05 web1 sshd[811]: Failed password for root from 2001:db8::7 port 50011 ssh2' + - 'Sep 28 10:00:10 web1 sshd[811]: Failed password for root from 2001:db8::7 port 50012 ssh2' + - 'Sep 28 10:00:15 web1 sshd[811]: Failed password for root from 2001:db8::7 port 50013 ssh2' + - 'Sep 28 10:00:20 web1 sshd[811]: Failed password for root from 2001:db8::7 port 50014 ssh2' + - 'Sep 28 10:00:25 web1 sshd[811]: Failed password for root from 2001:db8::7 port 50015 ssh2' - description: 5 failures spread over 4 minutes (slow guessing stays under the threshold) wazuh_skip: "wazuh correlates on analysis time; logtest processes every line at once, so time gaps can't be reproduced (covered by the Python engine)" expect: no_alert diff --git a/tests/samples/linux/ssh_failed_password.yml b/tests/samples/linux/ssh_failed_password.yml index 8ed6789..5374607 100644 --- a/tests/samples/linux/ssh_failed_password.yml +++ b/tests/samples/linux/ssh_failed_password.yml @@ -7,6 +7,9 @@ cases: - description: failed password for a non-existent user expect: match raw: "Sep 28 10:00:01 web1 sshd[811]: Failed password for invalid user admin from 203.0.113.7 port 51240 ssh2" + - description: failed password from an IPv6 source + expect: match + raw: "Sep 28 10:00:01 web1 sshd[811]: Failed password for root from 2001:db8::7 port 51241 ssh2" - description: successful login expect: no_match raw: "Sep 28 10:00:02 web1 sshd[812]: Accepted password for alice from 198.51.100.4 port 40022 ssh2" diff --git a/tests/test_scan_cli.py b/tests/test_scan_cli.py new file mode 100644 index 0000000..95b863a --- /dev/null +++ b/tests/test_scan_cli.py @@ -0,0 +1,36 @@ +"""CLI entry point of detlab.scan (file, stdin, exit codes, --year).""" + +import io + +from detlab.scan import main + +BURST = "".join( + f"Sep 28 10:00:0{i} web1 sshd[1]: Failed password for root from 2001:db8::7 port 5000{i} ssh2\n" + for i in range(6) +) + + +def test_main_reads_file_and_reports(tmp_path, capsys): + log = tmp_path / "auth.log" + log.write_text(BURST + "garbage line\n") + assert main([str(log), "--year", "2026"]) == 1 + out, err = capsys.readouterr() + assert "SSH Brute Force From Single Source" in out + assert "2001:db8::7" in out and "2026-09-28" in out + assert "1 line(s) in unknown formats skipped" in err + + +def test_main_reads_stdin(monkeypatch, capsys): + monkeypatch.setattr("sys.stdin", io.StringIO(BURST)) + assert main(["-"]) == 1 + assert "SSH Brute Force" in capsys.readouterr().out + + +def test_main_quiet_input_exits_zero(tmp_path, capsys): + log = tmp_path / "auth.log" + log.write_text( + "Sep 28 10:00:00 web1 sshd[1]: Accepted password for alice from 198.51.100.4 port 1 ssh2\n" + ) + assert main([str(log)]) == 0 + _, err = capsys.readouterr() + assert "0 alert(s); 0 line(s) in unknown formats skipped" in err