rsigma_runtime/input/
syslog.rs1use rsigma_eval::{JsonEvent, KvEvent};
15use syslog_loose::Message;
16
17use super::EventInputDecoded;
18
19#[derive(Debug, Clone, Default, PartialEq, Eq)]
21pub struct SyslogConfig {
22 pub default_tz_offset_secs: i32,
25}
26
27pub fn parse_syslog(line: &str, config: &SyslogConfig) -> EventInputDecoded {
33 let tz = chrono::FixedOffset::east_opt(config.default_tz_offset_secs)
34 .unwrap_or(chrono::FixedOffset::east_opt(0).unwrap());
35
36 let parsed = syslog_loose::parse_message_with_year_tz(
37 line,
38 resolve_year,
39 Some(tz),
40 syslog_loose::Variant::Either,
41 );
42
43 build_event_from_message(&parsed)
44}
45
46fn build_event_from_message(parsed: &Message<&str>) -> EventInputDecoded {
48 let msg_str = parsed.msg.trim();
49
50 if let Ok(mut json_obj) = serde_json::from_str::<serde_json::Value>(msg_str)
52 && let Some(obj) = json_obj.as_object_mut()
53 {
54 inject_syslog_headers(parsed, obj);
55 return EventInputDecoded::Json(JsonEvent::owned(serde_json::Value::Object(obj.clone())));
56 }
57
58 let mut fields = Vec::new();
60
61 if let Some(ts) = &parsed.timestamp {
62 fields.push(("timestamp".to_string(), ts.to_rfc3339()));
63 }
64 if let Some(host) = &parsed.hostname {
65 fields.push(("hostname".to_string(), host.to_string()));
66 }
67 if let Some(app) = &parsed.appname {
68 fields.push(("appname".to_string(), app.to_string()));
69 }
70 if let Some(pid) = &parsed.procid {
71 fields.push(("procid".to_string(), pid.to_string()));
72 }
73 if let Some(mid) = &parsed.msgid {
74 fields.push(("msgid".to_string(), mid.to_string()));
75 }
76 if let Some(facility) = &parsed.facility {
77 fields.push(("facility".to_string(), format!("{facility:?}")));
78 }
79 if let Some(severity) = &parsed.severity {
80 fields.push(("severity".to_string(), format!("{severity:?}")));
81 }
82
83 for elem in &parsed.structured_data {
85 for (key, val) in elem.params() {
86 let prefixed_key = format!("{}.{}", elem.id, key);
87 fields.push((prefixed_key, val));
88 }
89 }
90
91 if !msg_str.is_empty() {
92 fields.push(("_raw".to_string(), msg_str.to_string()));
93 }
94
95 EventInputDecoded::Kv(KvEvent::new(fields))
96}
97
98fn inject_syslog_headers(
103 parsed: &Message<&str>,
104 obj: &mut serde_json::Map<String, serde_json::Value>,
105) {
106 if let Some(ts) = &parsed.timestamp {
107 obj.entry("syslog_timestamp")
108 .or_insert_with(|| serde_json::Value::String(ts.to_rfc3339()));
109 }
110 if let Some(host) = &parsed.hostname {
111 obj.entry("syslog_hostname")
112 .or_insert_with(|| serde_json::Value::String(host.to_string()));
113 }
114 if let Some(app) = &parsed.appname {
115 obj.entry("syslog_appname")
116 .or_insert_with(|| serde_json::Value::String(app.to_string()));
117 }
118 if let Some(pid) = &parsed.procid {
119 obj.entry("syslog_procid")
120 .or_insert_with(|| serde_json::Value::String(pid.to_string()));
121 }
122 if let Some(mid) = &parsed.msgid {
123 obj.entry("syslog_msgid")
124 .or_insert_with(|| serde_json::Value::String(mid.to_string()));
125 }
126 if let Some(facility) = &parsed.facility {
127 obj.entry("syslog_facility")
128 .or_insert_with(|| serde_json::Value::String(format!("{facility:?}")));
129 }
130 if let Some(severity) = &parsed.severity {
131 obj.entry("syslog_severity")
132 .or_insert_with(|| serde_json::Value::String(format!("{severity:?}")));
133 }
134
135 for elem in &parsed.structured_data {
137 for (key, val) in elem.params() {
138 let prefixed_key = format!("sd.{}.{}", elem.id, key);
139 obj.entry(prefixed_key)
140 .or_insert_with(|| serde_json::Value::String(val));
141 }
142 }
143}
144
145fn resolve_year(date: syslog_loose::IncompleteDate) -> i32 {
151 let now = chrono::Utc::now();
152 let current_year = chrono::Datelike::year(&now);
153 let current_month = chrono::Datelike::month(&now);
154 let parsed_month = date.0;
155
156 if current_month == 12 && parsed_month == 1 {
157 current_year + 1
158 } else if current_month == 1 && parsed_month == 12 {
159 current_year - 1
160 } else {
161 current_year
162 }
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168 use rsigma_eval::Event;
169
170 #[test]
171 fn rfc5424_basic() {
172 let line = "<165>1 2024-01-15T10:30:00.000Z web01 myapp 1234 ID47 - Connection established";
173 let decoded = parse_syslog(line, &SyslogConfig::default());
174 assert!(decoded.get_field("hostname").is_some());
175 assert!(decoded.get_field("appname").is_some());
176 assert!(decoded.get_field("_raw").is_some());
177 }
178
179 #[test]
180 fn rfc3164_basic() {
181 let line = "<34>Oct 11 22:14:15 mymachine su: 'su root' failed for lonvick on /dev/pts/8";
182 let decoded = parse_syslog(line, &SyslogConfig::default());
183 assert!(decoded.any_string_value(&|s| s.contains("su root")));
184 }
185
186 #[test]
187 fn syslog_wrapped_json() {
188 let line = r#"<134>1 2024-01-15T10:30:00Z docker01 myapp 9876 MSGID1 - {"EventID": 1, "user": "admin"}"#;
189 let decoded = parse_syslog(line, &SyslogConfig::default());
190 assert!(decoded.get_field("EventID").is_some());
191 assert!(decoded.get_field("user").is_some());
192 assert!(decoded.get_field("syslog_hostname").is_some());
194 assert!(decoded.get_field("syslog_appname").is_some());
195 }
196
197 #[test]
198 fn rfc5424_structured_data() {
199 let line = r#"<165>1 2024-01-15T10:30:00Z host app - ID1 [exampleSDID@32473 iut="3" eventSource="App" eventID="1011"] message"#;
200 let decoded = parse_syslog(line, &SyslogConfig::default());
201 let json = decoded.to_json();
202 let json_str = serde_json::to_string(&json).unwrap();
203 assert!(json_str.contains("eventSource") || json_str.contains("_raw"));
204 }
205
206 #[test]
207 fn empty_msg() {
208 let line = "<13>1 2024-01-15T10:30:00Z host app - - -";
209 let decoded = parse_syslog(line, &SyslogConfig::default());
210 assert!(decoded.get_field("hostname").is_some());
211 }
212
213 #[test]
214 fn custom_timezone() {
215 let config = SyslogConfig {
216 default_tz_offset_secs: 5 * 3600, };
218 let line = "<34>Oct 11 22:14:15 mymachine su: test message";
219 let decoded = parse_syslog(line, &config);
220 assert!(decoded.any_string_value(&|s| s.contains("test message")));
221 }
222}