logcheck_fluent_bit_filter/
lib.rs1pub mod cli;
2pub mod regex_conversion;
3pub mod rules;
4
5#[cfg(test)]
6mod external_test;
7
8#[cfg(test)]
9mod production_test;
10
11use once_cell::sync::Lazy;
12use rules::{LogcheckDatabase, RuleCategory};
13use serde::{Deserialize, Serialize};
14use std::ffi::CString;
15use std::os::raw::c_char;
16
17#[derive(Debug, Deserialize, Serialize)]
19struct LogEntry {
20 #[serde(rename = "@timestamp")]
21 timestamp: String,
22 #[serde(rename = "MESSAGE")]
23 message: String,
24 #[serde(rename = "PRIORITY")]
25 priority: Option<String>,
26 #[serde(rename = "SYSTEMD_UNIT")]
27 systemd_unit: Option<String>,
28 #[serde(rename = "PID")]
29 pid: Option<String>,
30 #[serde(rename = "HOSTNAME")]
31 hostname: Option<String>,
32 #[serde(rename = "SYSLOG_IDENTIFIER")]
33 syslog_identifier: Option<String>,
34}
35
36static LOGCHECK_DB: Lazy<LogcheckDatabase> = Lazy::new(|| {
39 let mut db = LogcheckDatabase::new();
40
41 let _ = db
46 .violations_rules
47 .add_pattern("^.*authentication failure.*$".to_string());
48 let _ = db
49 .violations_rules
50 .add_pattern("^.*[Ff]ailed password.*$".to_string());
51 let _ = db
52 .violations_rules
53 .add_pattern("^.*sudo.*authentication failure.*$".to_string());
54 let _ = db
55 .violations_rules
56 .add_pattern("^.*segfault|segmentation fault.*$".to_string());
57 let _ = db
58 .violations_rules
59 .add_pattern("^.*Out of memory|OOM.*$".to_string());
60
61 let _ = db
63 .server
64 .add_pattern("^.*Started Session \\d+ of user.*$".to_string());
65 let _ = db
66 .server
67 .add_pattern("^.*Stopped Session \\d+ of user.*$".to_string());
68 let _ = db
69 .server
70 .add_pattern("^.*systemd-logind.*: New session.*of user.*$".to_string());
71 let _ = db
72 .server
73 .add_pattern("^.*systemd-logind.*: Session.*logged out.*$".to_string());
74 let _ = db
75 .server
76 .add_pattern("^.*sshd.*: Accepted (password|publickey) for.*$".to_string());
77 let _ = db
78 .server
79 .add_pattern("^.*sshd.*: Connection closed by.*$".to_string());
80
81 let _ = db.compile_all();
83
84 db
85});
86
87fn apply_logcheck_rules(log_entry: &LogEntry) -> Option<RuleCategory> {
89 LOGCHECK_DB.match_message(&log_entry.message)
90}
91
92fn create_filtered_entry(log_entry: &LogEntry, matched_category: Option<RuleCategory>) -> String {
94 let mut filtered_entry = serde_json::to_value(log_entry).unwrap();
95
96 if let Some(category) = matched_category {
97 let (rule_type, description) = match category {
98 RuleCategory::Cracking => ("cracking", "Active intrusion attempt detected"),
99 RuleCategory::CrackingIgnore => (
100 "cracking_ignore",
101 "Known false positive for cracking detection",
102 ),
103 RuleCategory::Violations => {
104 ("violations", "Security violation or critical system event")
105 }
106 RuleCategory::ViolationsIgnore => (
107 "violations_ignore",
108 "Known false positive for security violation",
109 ),
110 RuleCategory::SystemEvents => ("ignore", "Normal system event"),
111 RuleCategory::Workstation => ("ignore", "Normal workstation event"),
112 RuleCategory::Server => ("ignore", "Normal server event"),
113 RuleCategory::Local => ("ignore", "Local custom rule match"),
114 };
115
116 filtered_entry["logcheck_rule_type"] = serde_json::Value::String(rule_type.to_string());
117 filtered_entry["logcheck_description"] = serde_json::Value::String(description.to_string());
118 filtered_entry["logcheck_matched"] = serde_json::Value::Bool(true);
119
120 filtered_entry["logcheck_category"] = serde_json::Value::String(format!("{:?}", category));
122 } else {
123 filtered_entry["logcheck_matched"] = serde_json::Value::Bool(false);
124 filtered_entry["logcheck_rule_type"] =
125 serde_json::Value::String("unclassified".to_string());
126 filtered_entry["logcheck_description"] =
127 serde_json::Value::String("No matching logcheck rule found".to_string());
128 }
129
130 serde_json::to_string(&filtered_entry).unwrap_or_else(|_| "{}".to_string())
131}
132
133fn parse_log_entry(record: *const c_char, record_len: usize) -> Option<LogEntry> {
135 if record.is_null() || record_len == 0 {
136 return None;
137 }
138
139 let record_slice = unsafe { std::slice::from_raw_parts(record as *const u8, record_len) };
140 let record_str = std::str::from_utf8(record_slice).ok()?;
141
142 serde_json::from_str::<LogEntry>(record_str).ok()
143}
144
145fn to_c_string(s: String) -> *const c_char {
147 let boxed = CString::new(s)
148 .unwrap_or_else(|_| CString::new("{}").unwrap())
149 .into_boxed_c_str();
150 Box::leak(boxed).as_ptr()
151}
152
153#[unsafe(no_mangle)]
156pub extern "C" fn logcheck_filter_json(
157 _tag: *const c_char,
158 _tag_len: usize,
159 _time_sec: u32,
160 _time_nsec: u32,
161 record: *const c_char,
162 record_len: usize,
163) -> *const c_char {
164 let log_entry = match parse_log_entry(record, record_len) {
166 Some(entry) => entry,
167 None => {
168 let record_slice =
170 unsafe { std::slice::from_raw_parts(record as *const u8, record_len) };
171 let record_str = std::str::from_utf8(record_slice).unwrap_or("{}");
172 return to_c_string(record_str.to_string());
173 }
174 };
175
176 let matched_category = apply_logcheck_rules(&log_entry);
178
179 let filtered_json = create_filtered_entry(&log_entry, matched_category);
182
183 to_c_string(filtered_json)
184}
185
186#[unsafe(no_mangle)]
188pub extern "C" fn logcheck_demo_json(
189 _tag: *const c_char,
190 _tag_len: usize,
191 time_sec: u32,
192 _time_nsec: u32,
193 _record: *const c_char,
194 _record_len: usize,
195) -> *const c_char {
196 let demo_msg = format!(
197 r#"{{"logcheck_demo":"Hello from Rust WASM logcheck filter!","timestamp":{},"processed":true}}"#,
198 time_sec
199 );
200
201 to_c_string(demo_msg)
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207
208 #[test]
209 fn test_security_violation_matching() {
210 let log_entry = LogEntry {
211 timestamp: "2024-01-01T12:00:00Z".to_string(),
212 message: "sshd[1234]: Failed password for root from 192.168.1.1 port 22".to_string(),
213 priority: Some("4".to_string()),
214 systemd_unit: Some("ssh.service".to_string()),
215 pid: Some("1234".to_string()),
216 hostname: Some("testhost".to_string()),
217 syslog_identifier: Some("sshd".to_string()),
218 };
219
220 let matched_category = apply_logcheck_rules(&log_entry);
221 assert_eq!(matched_category, Some(RuleCategory::Violations));
222 }
223
224 #[test]
225 fn test_system_event_matching() {
226 let log_entry = LogEntry {
227 timestamp: "2024-01-01T12:00:00Z".to_string(),
228 message: "Started Session 123 of user alice.".to_string(),
229 priority: Some("6".to_string()),
230 systemd_unit: Some("systemd-logind.service".to_string()),
231 pid: Some("1".to_string()),
232 hostname: Some("testhost".to_string()),
233 syslog_identifier: Some("systemd-logind".to_string()),
234 };
235
236 let matched_category = apply_logcheck_rules(&log_entry);
237 assert_eq!(matched_category, Some(RuleCategory::SystemEvents));
238 }
239
240 #[test]
241 fn test_no_match() {
242 let log_entry = LogEntry {
243 timestamp: "2024-01-01T12:00:00Z".to_string(),
244 message: "Some random application message".to_string(),
245 priority: Some("6".to_string()),
246 systemd_unit: Some("myapp.service".to_string()),
247 pid: Some("9999".to_string()),
248 hostname: Some("testhost".to_string()),
249 syslog_identifier: Some("myapp".to_string()),
250 };
251
252 let matched_category = apply_logcheck_rules(&log_entry);
253 assert_eq!(matched_category, None);
254 }
255
256 #[test]
257 fn test_rule_database_stats() {
258 let stats = LOGCHECK_DB.get_stats();
259
260 assert!(stats["violations_rules"] > 0);
262 assert!(stats["server"] > 0);
263 assert!(stats["total_rules"] > 0);
264
265 println!("Rule database stats: {:?}", stats);
266 }
267}