diff --git a/CHANGELOG.md b/CHANGELOG.md index 771c115..ea8f0cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ All notable user-visible changes should be recorded here. - Added sanitized golden `report.md` / `report.json` regression fixtures to lock report contracts. - Added conservative parser coverage for `Accepted publickey` plus selected `pam_faillock` / `pam_sss` variants. +- Added compact host-level summaries to Markdown and JSON reports for multi-host inputs. ### Changed diff --git a/README.md b/README.md index 73428e2..e9335b6 100644 --- a/README.md +++ b/README.md @@ -106,7 +106,9 @@ The CLI writes: - `report.md` - `report.json` -into the output directory you provide. If you omit the output directory, the files are written into the current working directory. +into the output directory you provide. If you omit the output directory, the files are written into the current working directory. + +When an input spans multiple hostnames, both reports add compact host-level summaries without changing detector thresholds or introducing cross-host correlation logic. ## Sample Output diff --git a/src/main.cpp b/src/main.cpp index d5502b9..d396ecd 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -153,7 +153,8 @@ int main(int argc, char* argv[]) { parsed.quality, parsed.events, findings, - parsed.warnings}; + parsed.warnings, + app_config.detector.auth_signal_mappings}; loglens::write_reports(report_data, options.output_directory); diff --git a/src/report.cpp b/src/report.cpp index 5e71a34..be8126d 100644 --- a/src/report.cpp +++ b/src/report.cpp @@ -4,14 +4,25 @@ #include #include #include +#include #include #include #include +#include +#include #include namespace loglens { namespace { +struct HostSummary { + std::string hostname; + std::size_t parsed_event_count = 0; + std::size_t finding_count = 0; + std::size_t warning_count = 0; + std::vector> event_counts; +}; + std::string escape_json(std::string_view value) { std::string escaped; escaped.reserve(value.size()); @@ -125,6 +136,190 @@ std::string format_parse_success_percent(double rate) { return output.str(); } +std::string_view trim_left(std::string_view value) { + while (!value.empty() && (value.front() == ' ' || value.front() == '\t')) { + value.remove_prefix(1); + } + return value; +} + +std::string_view consume_token(std::string_view& input) { + input = trim_left(input); + if (input.empty()) { + return {}; + } + + const auto separator = input.find(' '); + if (separator == std::string_view::npos) { + const auto token = input; + input = {}; + return token; + } + + const auto token = input.substr(0, separator); + input.remove_prefix(separator + 1); + return token; +} + +std::optional extract_hostname_from_input_line(std::string_view line, InputMode input_mode) { + auto remaining = line; + switch (input_mode) { + case InputMode::SyslogLegacy: + if (consume_token(remaining).empty() + || consume_token(remaining).empty() + || consume_token(remaining).empty()) { + return std::nullopt; + } + break; + case InputMode::JournalctlShortFull: + if (consume_token(remaining).empty() + || consume_token(remaining).empty() + || consume_token(remaining).empty() + || consume_token(remaining).empty()) { + return std::nullopt; + } + break; + default: + return std::nullopt; + } + + const auto hostname = consume_token(remaining); + if (hostname.empty()) { + return std::nullopt; + } + + return std::string(hostname); +} + +std::unordered_map load_hostnames_by_line(const ReportData& data) { + std::unordered_map hostnames_by_line; + if (data.warnings.empty()) { + return hostnames_by_line; + } + + std::ifstream input(data.input_path); + if (!input) { + return hostnames_by_line; + } + + std::string line; + std::size_t line_number = 0; + while (std::getline(input, line)) { + ++line_number; + const auto hostname = extract_hostname_from_input_line(line, data.parse_metadata.input_mode); + if (hostname.has_value()) { + hostnames_by_line.emplace(line_number, *hostname); + } + } + + return hostnames_by_line; +} + +bool is_matching_finding_signal(const Finding& finding, const AuthSignal& signal) { + if (signal.timestamp < finding.first_seen || signal.timestamp > finding.last_seen) { + return false; + } + + switch (finding.type) { + case FindingType::BruteForce: + return signal.counts_as_terminal_auth_failure + && signal.source_ip == finding.subject; + case FindingType::MultiUserProbing: + if (!signal.counts_as_attempt_evidence || signal.source_ip != finding.subject) { + return false; + } + if (finding.usernames.empty()) { + return true; + } + return std::find( + finding.usernames.begin(), + finding.usernames.end(), + signal.username) + != finding.usernames.end(); + case FindingType::SudoBurst: + return signal.counts_as_sudo_burst_evidence + && signal.username == finding.subject; + default: + return false; + } +} + +std::vector build_host_summaries(const ReportData& data) { + std::unordered_map summaries_by_host; + + for (const auto& event : data.events) { + if (event.hostname.empty()) { + continue; + } + + auto& summary = summaries_by_host[event.hostname]; + summary.hostname = event.hostname; + ++summary.parsed_event_count; + } + + const auto hostnames_by_line = load_hostnames_by_line(data); + for (const auto& warning : data.warnings) { + const auto hostname_it = hostnames_by_line.find(warning.line_number); + if (hostname_it == hostnames_by_line.end() || hostname_it->second.empty()) { + continue; + } + + auto& summary = summaries_by_host[hostname_it->second]; + summary.hostname = hostname_it->second; + ++summary.warning_count; + } + + if (summaries_by_host.size() <= 1) { + return {}; + } + + std::unordered_map hostname_by_event_line; + hostname_by_event_line.reserve(data.events.size()); + std::unordered_map> events_by_host; + events_by_host.reserve(summaries_by_host.size()); + + for (const auto& event : data.events) { + hostname_by_event_line.emplace(event.line_number, event.hostname); + events_by_host[event.hostname].push_back(event); + } + + const auto signals = build_auth_signals(data.events, data.auth_signal_mappings); + for (const auto& finding : data.findings) { + std::unordered_set matching_hosts; + for (const auto& signal : signals) { + if (!is_matching_finding_signal(finding, signal)) { + continue; + } + + const auto hostname_it = hostname_by_event_line.find(signal.line_number); + if (hostname_it == hostname_by_event_line.end() || hostname_it->second.empty()) { + continue; + } + matching_hosts.insert(hostname_it->second); + } + + for (const auto& hostname : matching_hosts) { + ++summaries_by_host[hostname].finding_count; + } + } + + std::vector summaries; + summaries.reserve(summaries_by_host.size()); + for (auto& [hostname, summary] : summaries_by_host) { + const auto events_it = events_by_host.find(hostname); + if (events_it != events_by_host.end()) { + summary.event_counts = build_event_counts(events_it->second); + } + summaries.push_back(std::move(summary)); + } + + std::sort(summaries.begin(), summaries.end(), [](const HostSummary& left, const HostSummary& right) { + return left.hostname < right.hostname; + }); + + return summaries; +} + } // namespace std::string render_markdown_report(const ReportData& data) { @@ -132,6 +327,7 @@ std::string render_markdown_report(const ReportData& data) { const auto findings = sorted_findings(data.findings); const auto warnings = sorted_warnings(data.warnings); const auto event_counts = build_event_counts(data.events); + const auto host_summaries = build_host_summaries(data); output << "# LogLens Report\n\n"; output << "## Summary\n\n"; @@ -149,6 +345,19 @@ std::string render_markdown_report(const ReportData& data) { output << "- Findings: " << findings.size() << '\n'; output << "- Parser warnings: " << warnings.size() << "\n\n"; + if (!host_summaries.empty()) { + output << "## Host Summary\n\n"; + output << "| Host | Parsed Events | Findings | Warnings |\n"; + output << "| --- | ---: | ---: | ---: |\n"; + for (const auto& summary : host_summaries) { + output << "| " << summary.hostname + << " | " << summary.parsed_event_count + << " | " << summary.finding_count + << " | " << summary.warning_count << " |\n"; + } + output << '\n'; + } + output << "## Findings\n\n"; if (findings.empty()) { output << "No configured detections matched the analyzed events.\n\n"; @@ -205,6 +414,7 @@ std::string render_json_report(const ReportData& data) { const auto findings = sorted_findings(data.findings); const auto warnings = sorted_warnings(data.warnings); const auto event_counts = build_event_counts(data.events); + const auto host_summaries = build_host_summaries(data); output << "{\n"; output << " \"tool\": \"LogLens\",\n"; @@ -236,7 +446,31 @@ std::string render_json_report(const ReportData& data) { output << " {\"event_type\": \"" << to_string(type) << "\", \"count\": " << count << "}"; output << (index + 1 == event_counts.size() ? "\n" : ",\n"); } - output << " ],\n"; + output << " ]"; + if (!host_summaries.empty()) { + output << ",\n"; + output << " \"host_summaries\": [\n"; + for (std::size_t host_index = 0; host_index < host_summaries.size(); ++host_index) { + const auto& summary = host_summaries[host_index]; + output << " {\n"; + output << " \"hostname\": \"" << escape_json(summary.hostname) << "\",\n"; + output << " \"parsed_event_count\": " << summary.parsed_event_count << ",\n"; + output << " \"finding_count\": " << summary.finding_count << ",\n"; + output << " \"warning_count\": " << summary.warning_count << ",\n"; + output << " \"event_counts\": [\n"; + for (std::size_t event_index = 0; event_index < summary.event_counts.size(); ++event_index) { + const auto& [type, count] = summary.event_counts[event_index]; + output << " {\"event_type\": \"" << to_string(type) << "\", \"count\": " << count << "}"; + output << (event_index + 1 == summary.event_counts.size() ? "\n" : ",\n"); + } + output << " ]\n"; + output << " }"; + output << (host_index + 1 == host_summaries.size() ? "\n" : ",\n"); + } + output << " ],\n"; + } else { + output << ",\n"; + } output << " \"findings\": [\n"; for (std::size_t index = 0; index < findings.size(); ++index) { const auto& finding = findings[index]; diff --git a/src/report.hpp b/src/report.hpp index 3517ebc..47d8368 100644 --- a/src/report.hpp +++ b/src/report.hpp @@ -1,5 +1,6 @@ #pragma once +#include "signal.hpp" #include "detector.hpp" #include "parser.hpp" @@ -16,6 +17,7 @@ struct ReportData { std::vector events; std::vector findings; std::vector warnings; + AuthSignalConfig auth_signal_mappings; }; std::string render_markdown_report(const ReportData& data); diff --git a/tests/fixtures/report_contracts/multi_host_journalctl_short_full/input.log b/tests/fixtures/report_contracts/multi_host_journalctl_short_full/input.log new file mode 100644 index 0000000..8c9b818 --- /dev/null +++ b/tests/fixtures/report_contracts/multi_host_journalctl_short_full/input.log @@ -0,0 +1,11 @@ +Wed 2026-03-11 09:00:00 UTC alpha-host sshd[2301]: Failed password for invalid user admin from 203.0.113.10 port 52022 ssh2 +Wed 2026-03-11 09:01:05 UTC alpha-host sshd[2302]: Failed password for root from 203.0.113.10 port 52030 ssh2 +Wed 2026-03-11 09:02:10 UTC alpha-host sshd[2303]: Failed password for test from 203.0.113.10 port 52040 ssh2 +Wed 2026-03-11 09:03:44 UTC alpha-host sshd[2304]: Failed password for guest from 203.0.113.10 port 52050 ssh2 +Wed 2026-03-11 09:04:05 UTC alpha-host sshd[2305]: Failed password for invalid user deploy from 203.0.113.10 port 52060 ssh2 +Wed 2026-03-11 09:10:10 UTC beta-host sshd[2401]: Accepted publickey for alice from 203.0.113.20 port 52111 ssh2 +Wed 2026-03-11 09:11:00 UTC beta-host sudo: alice : TTY=pts/0 ; PWD=/home/alice ; USER=root ; COMMAND=/usr/bin/systemctl restart ssh +Wed 2026-03-11 09:12:10 UTC beta-host sudo: alice : TTY=pts/0 ; PWD=/home/alice ; USER=root ; COMMAND=/usr/bin/journalctl -xe +Wed 2026-03-11 09:14:15 UTC beta-host sudo: alice : TTY=pts/0 ; PWD=/home/alice ; USER=root ; COMMAND=/usr/bin/vi /etc/ssh/sshd_config +Wed 2026-03-11 09:15:12 UTC alpha-host sshd[2306]: Connection closed by authenticating user alice 203.0.113.50 port 52290 [preauth] +Wed 2026-03-11 09:16:18 UTC beta-host sshd[2402]: Timeout, client not responding from 203.0.113.51 port 52291 diff --git a/tests/fixtures/report_contracts/multi_host_journalctl_short_full/report.json b/tests/fixtures/report_contracts/multi_host_journalctl_short_full/report.json new file mode 100644 index 0000000..0ab874d --- /dev/null +++ b/tests/fixtures/report_contracts/multi_host_journalctl_short_full/report.json @@ -0,0 +1,83 @@ +{ + "tool": "LogLens", + "input": "tests/fixtures/report_contracts/multi_host_journalctl_short_full/input.log", + "input_mode": "journalctl_short_full", + "timezone_present": true, + "parser_quality": { + "total_lines": 11, + "parsed_lines": 9, + "unparsed_lines": 2, + "parse_success_rate": 0.8182, + "top_unknown_patterns": [ + {"pattern": "sshd_connection_closed_preauth", "count": 1}, + {"pattern": "sshd_timeout_or_disconnection", "count": 1} + ] + }, + "parsed_event_count": 9, + "warning_count": 2, + "finding_count": 3, + "event_counts": [ + {"event_type": "ssh_failed_password", "count": 3}, + {"event_type": "ssh_accepted_publickey", "count": 1}, + {"event_type": "ssh_invalid_user", "count": 2}, + {"event_type": "sudo_command", "count": 3} + ], + "host_summaries": [ + { + "hostname": "alpha-host", + "parsed_event_count": 5, + "finding_count": 2, + "warning_count": 1, + "event_counts": [ + {"event_type": "ssh_failed_password", "count": 3}, + {"event_type": "ssh_invalid_user", "count": 2} + ] + }, + { + "hostname": "beta-host", + "parsed_event_count": 4, + "finding_count": 1, + "warning_count": 1, + "event_counts": [ + {"event_type": "ssh_accepted_publickey", "count": 1}, + {"event_type": "sudo_command", "count": 3} + ] + } + ], + "findings": [ + { + "rule": "brute_force", + "subject_kind": "source_ip", + "subject": "203.0.113.10", + "event_count": 5, + "window_start": "2026-03-11 09:00:00", + "window_end": "2026-03-11 09:04:05", + "usernames": [], + "summary": "5 failed SSH attempts from 203.0.113.10 within 10 minutes." + }, + { + "rule": "multi_user_probing", + "subject_kind": "source_ip", + "subject": "203.0.113.10", + "event_count": 5, + "window_start": "2026-03-11 09:00:00", + "window_end": "2026-03-11 09:04:05", + "usernames": ["admin", "deploy", "guest", "root", "test"], + "summary": "203.0.113.10 targeted 5 usernames within 15 minutes." + }, + { + "rule": "sudo_burst", + "subject_kind": "username", + "subject": "alice", + "event_count": 3, + "window_start": "2026-03-11 09:11:00", + "window_end": "2026-03-11 09:14:15", + "usernames": [], + "summary": "alice ran 3 sudo commands within 5 minutes." + } + ], + "warnings": [ + {"line_number": 10, "reason": "unrecognized auth pattern: sshd_connection_closed_preauth"}, + {"line_number": 11, "reason": "unrecognized auth pattern: sshd_timeout_or_disconnection"} + ] +} diff --git a/tests/fixtures/report_contracts/multi_host_journalctl_short_full/report.md b/tests/fixtures/report_contracts/multi_host_journalctl_short_full/report.md new file mode 100644 index 0000000..7326801 --- /dev/null +++ b/tests/fixtures/report_contracts/multi_host_journalctl_short_full/report.md @@ -0,0 +1,52 @@ +# LogLens Report + +## Summary + +- Input: `tests/fixtures/report_contracts/multi_host_journalctl_short_full/input.log` +- Input mode: journalctl_short_full +- Timezone present: true +- Total lines: 11 +- Parsed lines: 9 +- Unparsed lines: 2 +- Parse success rate: 81.82% +- Parsed events: 9 +- Findings: 3 +- Parser warnings: 2 + +## Host Summary + +| Host | Parsed Events | Findings | Warnings | +| --- | ---: | ---: | ---: | +| alpha-host | 5 | 2 | 1 | +| beta-host | 4 | 1 | 1 | + +## Findings + +| Rule | Subject | Count | Window | Notes | +| --- | --- | ---: | --- | --- | +| brute_force | 203.0.113.10 | 5 | 2026-03-11 09:00:00 -> 2026-03-11 09:04:05 | 5 failed SSH attempts from 203.0.113.10 within 10 minutes. | +| multi_user_probing | 203.0.113.10 | 5 | 2026-03-11 09:00:00 -> 2026-03-11 09:04:05 | 203.0.113.10 targeted 5 usernames within 15 minutes. Usernames: admin, deploy, guest, root, test | +| sudo_burst | alice | 3 | 2026-03-11 09:11:00 -> 2026-03-11 09:14:15 | alice ran 3 sudo commands within 5 minutes. | + +## Event Counts + +| Event Type | Count | +| --- | ---: | +| ssh_failed_password | 3 | +| ssh_accepted_publickey | 1 | +| ssh_invalid_user | 2 | +| sudo_command | 3 | + +## Parser Quality + +| Unknown Pattern | Count | +| --- | ---: | +| sshd_connection_closed_preauth | 1 | +| sshd_timeout_or_disconnection | 1 | + +## Parser Warnings + +| Line | Reason | +| ---: | --- | +| 10 | unrecognized auth pattern: sshd_connection_closed_preauth | +| 11 | unrecognized auth pattern: sshd_timeout_or_disconnection | diff --git a/tests/fixtures/report_contracts/multi_host_syslog_legacy/input.log b/tests/fixtures/report_contracts/multi_host_syslog_legacy/input.log new file mode 100644 index 0000000..471a63d --- /dev/null +++ b/tests/fixtures/report_contracts/multi_host_syslog_legacy/input.log @@ -0,0 +1,11 @@ +Mar 11 09:00:00 alpha-host sshd[1301]: Failed password for invalid user admin from 203.0.113.10 port 52022 ssh2 +Mar 11 09:01:05 alpha-host sshd[1302]: Failed password for root from 203.0.113.10 port 52030 ssh2 +Mar 11 09:02:10 alpha-host sshd[1303]: Failed password for test from 203.0.113.10 port 52040 ssh2 +Mar 11 09:03:44 alpha-host sshd[1304]: Failed password for guest from 203.0.113.10 port 52050 ssh2 +Mar 11 09:04:05 alpha-host sshd[1305]: Failed password for invalid user deploy from 203.0.113.10 port 52060 ssh2 +Mar 11 09:10:10 beta-host sshd[1401]: Accepted publickey for alice from 203.0.113.20 port 52111 ssh2 +Mar 11 09:11:00 beta-host sudo: alice : TTY=pts/0 ; PWD=/home/alice ; USER=root ; COMMAND=/usr/bin/systemctl restart ssh +Mar 11 09:12:10 beta-host sudo: alice : TTY=pts/0 ; PWD=/home/alice ; USER=root ; COMMAND=/usr/bin/journalctl -xe +Mar 11 09:14:15 beta-host sudo: alice : TTY=pts/0 ; PWD=/home/alice ; USER=root ; COMMAND=/usr/bin/vi /etc/ssh/sshd_config +Mar 11 09:15:12 alpha-host sshd[1306]: Connection closed by authenticating user alice 203.0.113.50 port 52290 [preauth] +Mar 11 09:16:18 beta-host sshd[1402]: Timeout, client not responding from 203.0.113.51 port 52291 diff --git a/tests/fixtures/report_contracts/multi_host_syslog_legacy/report.json b/tests/fixtures/report_contracts/multi_host_syslog_legacy/report.json new file mode 100644 index 0000000..7af40d3 --- /dev/null +++ b/tests/fixtures/report_contracts/multi_host_syslog_legacy/report.json @@ -0,0 +1,84 @@ +{ + "tool": "LogLens", + "input": "tests/fixtures/report_contracts/multi_host_syslog_legacy/input.log", + "input_mode": "syslog_legacy", + "assume_year": 2026, + "timezone_present": false, + "parser_quality": { + "total_lines": 11, + "parsed_lines": 9, + "unparsed_lines": 2, + "parse_success_rate": 0.8182, + "top_unknown_patterns": [ + {"pattern": "sshd_connection_closed_preauth", "count": 1}, + {"pattern": "sshd_timeout_or_disconnection", "count": 1} + ] + }, + "parsed_event_count": 9, + "warning_count": 2, + "finding_count": 3, + "event_counts": [ + {"event_type": "ssh_failed_password", "count": 3}, + {"event_type": "ssh_accepted_publickey", "count": 1}, + {"event_type": "ssh_invalid_user", "count": 2}, + {"event_type": "sudo_command", "count": 3} + ], + "host_summaries": [ + { + "hostname": "alpha-host", + "parsed_event_count": 5, + "finding_count": 2, + "warning_count": 1, + "event_counts": [ + {"event_type": "ssh_failed_password", "count": 3}, + {"event_type": "ssh_invalid_user", "count": 2} + ] + }, + { + "hostname": "beta-host", + "parsed_event_count": 4, + "finding_count": 1, + "warning_count": 1, + "event_counts": [ + {"event_type": "ssh_accepted_publickey", "count": 1}, + {"event_type": "sudo_command", "count": 3} + ] + } + ], + "findings": [ + { + "rule": "brute_force", + "subject_kind": "source_ip", + "subject": "203.0.113.10", + "event_count": 5, + "window_start": "2026-03-11 09:00:00", + "window_end": "2026-03-11 09:04:05", + "usernames": [], + "summary": "5 failed SSH attempts from 203.0.113.10 within 10 minutes." + }, + { + "rule": "multi_user_probing", + "subject_kind": "source_ip", + "subject": "203.0.113.10", + "event_count": 5, + "window_start": "2026-03-11 09:00:00", + "window_end": "2026-03-11 09:04:05", + "usernames": ["admin", "deploy", "guest", "root", "test"], + "summary": "203.0.113.10 targeted 5 usernames within 15 minutes." + }, + { + "rule": "sudo_burst", + "subject_kind": "username", + "subject": "alice", + "event_count": 3, + "window_start": "2026-03-11 09:11:00", + "window_end": "2026-03-11 09:14:15", + "usernames": [], + "summary": "alice ran 3 sudo commands within 5 minutes." + } + ], + "warnings": [ + {"line_number": 10, "reason": "unrecognized auth pattern: sshd_connection_closed_preauth"}, + {"line_number": 11, "reason": "unrecognized auth pattern: sshd_timeout_or_disconnection"} + ] +} diff --git a/tests/fixtures/report_contracts/multi_host_syslog_legacy/report.md b/tests/fixtures/report_contracts/multi_host_syslog_legacy/report.md new file mode 100644 index 0000000..107cf71 --- /dev/null +++ b/tests/fixtures/report_contracts/multi_host_syslog_legacy/report.md @@ -0,0 +1,53 @@ +# LogLens Report + +## Summary + +- Input: `tests/fixtures/report_contracts/multi_host_syslog_legacy/input.log` +- Input mode: syslog_legacy +- Assume year: 2026 +- Timezone present: false +- Total lines: 11 +- Parsed lines: 9 +- Unparsed lines: 2 +- Parse success rate: 81.82% +- Parsed events: 9 +- Findings: 3 +- Parser warnings: 2 + +## Host Summary + +| Host | Parsed Events | Findings | Warnings | +| --- | ---: | ---: | ---: | +| alpha-host | 5 | 2 | 1 | +| beta-host | 4 | 1 | 1 | + +## Findings + +| Rule | Subject | Count | Window | Notes | +| --- | --- | ---: | --- | --- | +| brute_force | 203.0.113.10 | 5 | 2026-03-11 09:00:00 -> 2026-03-11 09:04:05 | 5 failed SSH attempts from 203.0.113.10 within 10 minutes. | +| multi_user_probing | 203.0.113.10 | 5 | 2026-03-11 09:00:00 -> 2026-03-11 09:04:05 | 203.0.113.10 targeted 5 usernames within 15 minutes. Usernames: admin, deploy, guest, root, test | +| sudo_burst | alice | 3 | 2026-03-11 09:11:00 -> 2026-03-11 09:14:15 | alice ran 3 sudo commands within 5 minutes. | + +## Event Counts + +| Event Type | Count | +| --- | ---: | +| ssh_failed_password | 3 | +| ssh_accepted_publickey | 1 | +| ssh_invalid_user | 2 | +| sudo_command | 3 | + +## Parser Quality + +| Unknown Pattern | Count | +| --- | ---: | +| sshd_connection_closed_preauth | 1 | +| sshd_timeout_or_disconnection | 1 | + +## Parser Warnings + +| Line | Reason | +| ---: | --- | +| 10 | unrecognized auth pattern: sshd_connection_closed_preauth | +| 11 | unrecognized auth pattern: sshd_timeout_or_disconnection | diff --git a/tests/test_report_contracts.cpp b/tests/test_report_contracts.cpp index e4985e3..8d8f9ce 100644 --- a/tests/test_report_contracts.cpp +++ b/tests/test_report_contracts.cpp @@ -147,6 +147,8 @@ std::vector extract_json_contract_lines(const std::string& json) { || starts_with(line, "\"parsed_event_count\": ") || starts_with(line, "\"warning_count\": ") || starts_with(line, "\"finding_count\": ") + || starts_with(line, "\"host_summaries\": ") + || starts_with(line, "\"hostname\": ") || starts_with(line, "{\"pattern\": ") || starts_with(line, "{\"event_type\": ") || starts_with(line, "\"rule\": ") @@ -261,6 +263,17 @@ int main(int argc, char* argv[]) { fixture_root / "journalctl_short_full", output_root, "journalctl-short-full"); + run_report_contract_case( + loglens_exe, + fixture_root / "multi_host_syslog_legacy", + output_root, + "syslog", + "--year 2026"); + run_report_contract_case( + loglens_exe, + fixture_root / "multi_host_journalctl_short_full", + output_root, + "journalctl-short-full"); } catch (...) { std::filesystem::current_path(original_cwd); throw;