Skip to content

Commit 91fe10a

Browse files
committed
test(config): lock sample config contract
1 parent 4057798 commit 91fe10a

2 files changed

Lines changed: 105 additions & 0 deletions

File tree

tests/test_cli.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,12 @@ int main(int argc, char* argv[]) {
225225
+ " " + quote_argument(config_run_out))
226226
.c_str());
227227
expect(config_run_exit == 0, "expected sample config run to succeed");
228+
expect_report_core_fields(
229+
read_file(config_run_out / "report.md"),
230+
read_file(config_run_out / "report.json"),
231+
"syslog_legacy",
232+
true,
233+
false);
228234

229235
const auto journalctl_out = output_dir / "journalctl_cli";
230236
std::filesystem::create_directories(journalctl_out);

tests/test_detector.cpp

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
#include <stdexcept>
1111
#include <string>
1212
#include <string_view>
13+
#include <vector>
1314

1415
namespace {
1516

@@ -49,6 +50,31 @@ std::vector<loglens::Event> parse_events(loglens::ParserConfig config, std::stri
4950
return parser.parse_stream(input).events;
5051
}
5152

53+
std::filesystem::path repo_root() {
54+
const std::filesystem::path source_path{__FILE__};
55+
std::vector<std::filesystem::path> candidates;
56+
57+
if (source_path.is_absolute()) {
58+
candidates.push_back(source_path);
59+
} else {
60+
const auto cwd = std::filesystem::current_path();
61+
candidates.push_back(cwd / source_path);
62+
candidates.push_back(cwd.parent_path() / source_path);
63+
}
64+
65+
for (const auto& candidate : candidates) {
66+
if (std::filesystem::exists(candidate)) {
67+
return candidate.parent_path().parent_path();
68+
}
69+
}
70+
71+
throw std::runtime_error("unable to resolve repository root from test source path");
72+
}
73+
74+
std::filesystem::path asset_path(std::string_view filename) {
75+
return repo_root() / "assets" / std::string(filename);
76+
}
77+
5278
loglens::ParserConfig make_syslog_config() {
5379
return loglens::ParserConfig{
5480
loglens::InputMode::SyslogLegacy,
@@ -121,6 +147,54 @@ std::vector<loglens::Event> build_sudo_burst_preservation_events() {
121147
"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");
122148
}
123149

150+
void expect_same_rule_threshold(const loglens::RuleThreshold& actual,
151+
const loglens::RuleThreshold& expected,
152+
const std::string& rule_name) {
153+
expect(actual.threshold == expected.threshold, "expected " + rule_name + " threshold to match default");
154+
expect(actual.window == expected.window, "expected " + rule_name + " window to match default");
155+
}
156+
157+
void expect_same_auth_signal_behavior(const loglens::AuthSignalBehavior& actual,
158+
const loglens::AuthSignalBehavior& expected,
159+
const std::string& signal_name) {
160+
expect(actual.counts_as_attempt_evidence == expected.counts_as_attempt_evidence,
161+
"expected " + signal_name + " attempt-evidence mapping to match default");
162+
expect(actual.counts_as_terminal_auth_failure == expected.counts_as_terminal_auth_failure,
163+
"expected " + signal_name + " terminal-failure mapping to match default");
164+
}
165+
166+
void expect_same_detector_config(const loglens::DetectorConfig& actual,
167+
const loglens::DetectorConfig& expected) {
168+
expect_same_rule_threshold(actual.brute_force, expected.brute_force, "brute_force");
169+
expect_same_rule_threshold(actual.multi_user_probing, expected.multi_user_probing, "multi_user_probing");
170+
expect_same_rule_threshold(actual.sudo_burst, expected.sudo_burst, "sudo_burst");
171+
172+
expect_same_auth_signal_behavior(
173+
actual.auth_signal_mappings.ssh_failed_password,
174+
expected.auth_signal_mappings.ssh_failed_password,
175+
"ssh_failed_password");
176+
expect_same_auth_signal_behavior(
177+
actual.auth_signal_mappings.ssh_invalid_user,
178+
expected.auth_signal_mappings.ssh_invalid_user,
179+
"ssh_invalid_user");
180+
expect_same_auth_signal_behavior(
181+
actual.auth_signal_mappings.ssh_failed_publickey,
182+
expected.auth_signal_mappings.ssh_failed_publickey,
183+
"ssh_failed_publickey");
184+
expect_same_auth_signal_behavior(
185+
actual.auth_signal_mappings.ssh_failed_keyboard_interactive,
186+
expected.auth_signal_mappings.ssh_failed_keyboard_interactive,
187+
"ssh_failed_keyboard_interactive");
188+
expect_same_auth_signal_behavior(
189+
actual.auth_signal_mappings.ssh_max_auth_tries,
190+
expected.auth_signal_mappings.ssh_max_auth_tries,
191+
"ssh_max_auth_tries");
192+
expect_same_auth_signal_behavior(
193+
actual.auth_signal_mappings.pam_auth_failure,
194+
expected.auth_signal_mappings.pam_auth_failure,
195+
"pam_auth_failure");
196+
}
197+
124198
void test_default_thresholds() {
125199
const auto events = build_events();
126200
const loglens::Detector detector;
@@ -341,6 +415,30 @@ void test_load_valid_config() {
341415
expect(findings.size() == 3, "expected loaded config to preserve default findings");
342416
}
343417

418+
void test_sample_config_matches_default_detector_contract() {
419+
const auto config = loglens::load_app_config(asset_path("sample_config.json"));
420+
expect(config.input_mode == loglens::InputMode::SyslogLegacy,
421+
"expected sample config to use syslog legacy input");
422+
expect(config.timestamp.assume_year == 2026,
423+
"expected sample config to provide the sample syslog year");
424+
expect_same_detector_config(config.detector, loglens::DetectorConfig{});
425+
426+
const auto events = build_events();
427+
const loglens::Detector default_detector;
428+
const loglens::Detector sample_config_detector(config.detector);
429+
const auto default_findings = default_detector.analyze(events);
430+
const auto sample_config_findings = sample_config_detector.analyze(events);
431+
432+
expect(sample_config_findings.size() == default_findings.size(),
433+
"expected sample config to preserve default finding count");
434+
expect(find_finding(sample_config_findings, loglens::FindingType::BruteForce, "203.0.113.10") != nullptr,
435+
"expected sample config to preserve brute-force finding");
436+
expect(find_finding(sample_config_findings, loglens::FindingType::MultiUserProbing, "203.0.113.10") != nullptr,
437+
"expected sample config to preserve multi-user finding");
438+
expect(find_finding(sample_config_findings, loglens::FindingType::SudoBurst, "alice") != nullptr,
439+
"expected sample config to preserve sudo-burst finding");
440+
}
441+
344442
void test_reject_invalid_config() {
345443
const auto temp_path = std::filesystem::current_path() / "invalid_config_test.json";
346444
{
@@ -389,6 +487,7 @@ int main() {
389487
test_pam_auth_failure_does_not_trigger_bruteforce_by_default();
390488
test_equivalent_attack_scenario_yields_same_finding_count_across_modes();
391489
test_load_valid_config();
490+
test_sample_config_matches_default_detector_contract();
392491
test_reject_invalid_config();
393492
return 0;
394493
}

0 commit comments

Comments
 (0)