postfix_log_parser/components/
proxymap.rs1use regex::Regex;
29
30use crate::events::proxymap::{ProxymapEvent, ProxymapEventType};
31
32#[derive(Debug)]
34pub struct ProxymapParser {
35 config_override_regex: Regex,
36}
37
38impl ProxymapParser {
39 pub fn new() -> Self {
41 Self {
42 config_override_regex: Regex::new(
43 r"^(.+?), line (\d+): overriding earlier entry: (.+?)=(.+)$",
44 )
45 .expect("Failed to compile config override regex"),
46 }
47 }
48
49 pub fn parse_log_line(&self, line: &str) -> Result<ProxymapEvent, String> {
51 let basic_regex = Regex::new(
53 r"^((?:\d{4}\s+)?\w{3}\s+\d{1,2}\s+\d{2}:\d{2}:\d{2}(?:\.\d+)?)\s+\S+\s+postfix/proxymap\[(\d+)\]:\s+warning:\s+(.+)$"
54 ).map_err(|e| format!("Regex compilation error: {}", e))?;
55
56 let captures = basic_regex
57 .captures(line)
58 .ok_or_else(|| "Line does not match proxymap log format".to_string())?;
59
60 let timestamp = captures.get(1).unwrap().as_str();
61 let process_id = captures.get(2).unwrap().as_str();
62 let message = captures.get(3).unwrap().as_str();
63
64 if let Some(event) = self.parse_config_override_warning(timestamp, process_id, message) {
66 return Ok(event);
67 }
68
69 Err(format!("Unknown proxymap message type: {}", message))
70 }
71
72 pub fn supported_event_types(&self) -> usize {
74 1
75 }
76
77 pub fn matches_component(&self, line: &str) -> bool {
79 line.contains("postfix/proxymap[")
80 }
81
82 fn parse_config_override_warning(
84 &self,
85 timestamp: &str,
86 process_id: &str,
87 message: &str,
88 ) -> Option<ProxymapEvent> {
89 if let Some(captures) = self.config_override_regex.captures(message) {
90 let file_path = captures.get(1).unwrap().as_str();
91 let line_number: u32 = captures.get(2).unwrap().as_str().parse().ok()?;
92 let parameter = captures.get(3).unwrap().as_str();
93 let value = captures.get(4).unwrap().as_str();
94
95 Some(ProxymapEvent {
96 timestamp: timestamp.to_string(),
97 process_id: process_id.to_string(),
98 event_type: ProxymapEventType::ConfigOverrideWarning {
99 file_path: file_path.to_string(),
100 line_number,
101 parameter: parameter.to_string(),
102 value: value.to_string(),
103 },
104 })
105 } else {
106 None
107 }
108 }
109}
110
111impl Default for ProxymapParser {
112 fn default() -> Self {
113 Self::new()
114 }
115}
116
117impl crate::components::ComponentParser for ProxymapParser {
118 fn parse(
119 &self,
120 message: &str,
121 ) -> Result<crate::events::base::ComponentEvent, crate::error::ParseError> {
122 if let Some(captures) = self.config_override_regex.captures(message) {
124 let file_path = captures.get(1).unwrap().as_str();
125 let line_number: u32 = captures.get(2).unwrap().as_str().parse().map_err(|_| {
126 crate::error::ParseError::ComponentParseError {
127 component: self.component_name().to_string(),
128 reason: "Invalid line number format".to_string(),
129 }
130 })?;
131 let parameter = captures.get(3).unwrap().as_str();
132 let value = captures.get(4).unwrap().as_str();
133
134 let event = ProxymapEvent {
135 timestamp: "0".to_string(), process_id: "0".to_string(), event_type: ProxymapEventType::ConfigOverrideWarning {
138 file_path: file_path.to_string(),
139 line_number,
140 parameter: parameter.to_string(),
141 value: value.to_string(),
142 },
143 };
144
145 return Ok(crate::events::base::ComponentEvent::Proxymap(event));
146 }
147
148 Err(crate::error::ParseError::ComponentParseError {
149 component: self.component_name().to_string(),
150 reason: format!("Unable to parse proxymap message: {}", message),
151 })
152 }
153
154 fn component_name(&self) -> &'static str {
155 "proxymap"
156 }
157
158 fn can_parse(&self, message: &str) -> bool {
159 self.config_override_regex.is_match(message)
160 }
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166 use crate::components::ComponentParser;
167
168 fn create_parser() -> ProxymapParser {
169 ProxymapParser::new()
170 }
171
172 #[test]
173 fn test_config_override_warning_parsing() {
174 let parser = create_parser();
175 let log_line = "Apr 08 17:54:42 m01 postfix/proxymap[80]: warning: /etc/postfix/main.cf, line 820: overriding earlier entry: smtpd_recipient_restrictions=check_client_access pcre:/etc/postfix/filter_trusted, permit_sasl_authenticated, permit_mynetworks, reject_unauth_destination, pcre:/etc/postfix/filter_default";
176
177 let result = parser.parse_log_line(log_line);
178 assert!(result.is_ok());
179
180 let event = result.unwrap();
181 assert_eq!(event.timestamp, "Apr 08 17:54:42");
182 assert_eq!(event.process_id, "80");
183
184 let ProxymapEventType::ConfigOverrideWarning {
185 file_path,
186 line_number,
187 parameter,
188 value,
189 } = event.event_type;
190
191 assert_eq!(file_path, "/etc/postfix/main.cf");
192 assert_eq!(line_number, 820);
193 assert_eq!(parameter, "smtpd_recipient_restrictions");
194 assert!(value.contains("check_client_access"));
195 assert!(value.contains("permit_sasl_authenticated"));
196 }
197
198 #[test]
199 fn test_client_message_rate_limit_parsing() {
200 let parser = create_parser();
201 let log_line = "Apr 10 11:17:43 m01 postfix/proxymap[80]: warning: /etc/postfix/main.cf, line 806: overriding earlier entry: smtpd_client_message_rate_limit=0";
202
203 let result = parser.parse_log_line(log_line);
204 assert!(result.is_ok());
205
206 let event = result.unwrap();
207 assert_eq!(event.timestamp, "Apr 10 11:17:43");
208 assert_eq!(event.process_id, "80");
209
210 let ProxymapEventType::ConfigOverrideWarning {
211 file_path,
212 line_number,
213 parameter,
214 value,
215 } = event.event_type;
216
217 assert_eq!(file_path, "/etc/postfix/main.cf");
218 assert_eq!(line_number, 806);
219 assert_eq!(parameter, "smtpd_client_message_rate_limit");
220 assert_eq!(value, "0");
221 }
222
223 #[test]
224 fn test_discard_ehlo_keywords_parsing() {
225 let parser = create_parser();
226 let log_line = "Apr 10 11:17:43 m01 postfix/proxymap[80]: warning: /etc/postfix/main.cf, line 826: overriding earlier entry: smtpd_discard_ehlo_keywords=silent-discard,dsn,etrn";
227
228 let result = parser.parse_log_line(log_line);
229 assert!(result.is_ok());
230
231 let event = result.unwrap();
232 let ProxymapEventType::ConfigOverrideWarning {
233 parameter, value, ..
234 } = event.event_type;
235
236 assert_eq!(parameter, "smtpd_discard_ehlo_keywords");
237 assert_eq!(value, "silent-discard,dsn,etrn");
238 }
239
240 #[test]
241 fn test_different_line_numbers() {
242 let parser = create_parser();
243 let test_cases = vec![
244 ("line 806", 806),
245 ("line 819", 819),
246 ("line 820", 820),
247 ("line 826", 826),
248 ];
249
250 for (line_text, expected_line) in test_cases {
251 let log_line = format!("Apr 10 11:17:43 m01 postfix/proxymap[80]: warning: /etc/postfix/main.cf, {}: overriding earlier entry: test_param=test_value", line_text);
252
253 let result = parser.parse_log_line(&log_line);
254 assert!(result.is_ok());
255
256 let event = result.unwrap();
257 let ProxymapEventType::ConfigOverrideWarning { line_number, .. } = event.event_type;
258 assert_eq!(line_number, expected_line);
259 }
260 }
261
262 #[test]
263 fn test_different_process_ids() {
264 let parser = create_parser();
265 let test_cases = vec!["80", "84"];
266
267 for pid in test_cases {
268 let log_line = format!("Apr 08 17:54:42 m01 postfix/proxymap[{}]: warning: /etc/postfix/main.cf, line 820: overriding earlier entry: test_param=test_value", pid);
269
270 let result = parser.parse_log_line(&log_line);
271 assert!(result.is_ok());
272
273 let event = result.unwrap();
274 assert_eq!(event.process_id, pid);
275 }
276 }
277
278 #[test]
279 fn test_component_matching() {
280 let parser = create_parser();
281
282 let matching_lines = vec![
284 "Apr 08 17:54:42 m01 postfix/proxymap[80]: warning: /etc/postfix/main.cf, line 820: overriding earlier entry: test=value",
285 "Apr 10 11:17:43 m01 postfix/proxymap[84]: warning: /etc/postfix/main.cf, line 806: overriding earlier entry: another=param",
286 ];
287
288 for line in matching_lines {
289 assert!(parser.matches_component(line), "Should match: {}", line);
290 }
291
292 let non_matching_lines = vec![
294 "Apr 08 17:54:42 m01 postfix/qmgr[78]: info: statistics",
295 "Apr 08 17:54:42 m01 postfix/smtpd[78]: connect from localhost",
296 "Apr 08 17:54:42 m01 postfix/cleanup[78]: message-id=<test@example.com>",
297 ];
298
299 for line in non_matching_lines {
300 assert!(
301 !parser.matches_component(line),
302 "Should not match: {}",
303 line
304 );
305 }
306 }
307
308 #[test]
309 fn test_invalid_log_lines() {
310 let parser = create_parser();
311
312 let invalid_lines = vec![
313 "Invalid log line",
314 "Apr 08 17:54:42 m01 postfix/qmgr[78]: info: statistics",
315 "Apr 08 17:54:42 m01 postfix/proxymap[80]: info: some other message",
316 "incomplete line",
317 ];
318
319 for line in invalid_lines {
320 let result = parser.parse_log_line(line);
321 assert!(result.is_err(), "Should fail to parse: {}", line);
322 }
323 }
324
325 #[test]
326 fn test_supported_event_types() {
327 let parser = create_parser();
328 assert_eq!(parser.supported_event_types(), 1);
329 }
330
331 #[test]
332 fn test_parser_default() {
333 let parser = ProxymapParser::default();
334 assert_eq!(parser.supported_event_types(), 1);
335 }
336
337 #[test]
338 fn test_component_parser_parse() {
339 let parser = ProxymapParser::new();
340
341 let message = "/etc/postfix/main.cf, line 820: overriding earlier entry: smtpd_recipient_restrictions=check_client_access pcre:/etc/postfix/filter_trusted";
343 let result = parser.parse(message);
344
345 assert!(result.is_ok());
346 match result.unwrap() {
347 crate::events::base::ComponentEvent::Proxymap(event) => {
348 assert_eq!(event.process_id, "0"); let ProxymapEventType::ConfigOverrideWarning {
350 file_path,
351 line_number,
352 parameter,
353 ..
354 } = event.event_type;
355
356 assert_eq!(file_path, "/etc/postfix/main.cf");
357 assert_eq!(line_number, 820);
358 assert_eq!(parameter, "smtpd_recipient_restrictions");
359 }
360 _ => panic!("Expected Proxymap ComponentEvent"),
361 }
362 }
363
364 #[test]
365 fn test_component_parser_invalid() {
366 let parser = ProxymapParser::new();
367
368 let message = "some invalid message";
369 let result = parser.parse(message);
370
371 assert!(result.is_err());
372 match result.unwrap_err() {
373 crate::error::ParseError::ComponentParseError { component, .. } => {
374 assert_eq!(component, "proxymap");
375 }
376 _ => panic!("Expected ComponentParseError"),
377 }
378 }
379
380 #[test]
381 fn test_component_name() {
382 let parser = ProxymapParser::new();
383 assert_eq!(parser.component_name(), "proxymap");
384 }
385
386 #[test]
387 fn test_can_parse() {
388 let parser = ProxymapParser::new();
389
390 assert!(parser
392 .can_parse("/etc/postfix/main.cf, line 820: overriding earlier entry: test=value"));
393
394 assert!(!parser.can_parse("some random message"));
396 assert!(!parser.can_parse("overriding earlier entry: test=value")); assert!(!parser.can_parse("/etc/postfix/main.cf, line 820: some other message"));
398 }
400
401 #[test]
402 fn test_parse_real_log_samples() {
403 let parser = create_parser();
404
405 let real_logs = vec![
407 "Apr 08 17:54:42 m01 postfix/proxymap[80]: warning: /etc/postfix/main.cf, line 820: overriding earlier entry: smtpd_recipient_restrictions=check_client_access pcre:/etc/postfix/filter_trusted, permit_sasl_authenticated, permit_mynetworks, reject_unauth_destination, pcre:/etc/postfix/filter_default",
408 "Apr 08 17:58:29 m01 postfix/proxymap[84]: warning: /etc/postfix/main.cf, line 820: overriding earlier entry: smtpd_recipient_restrictions=check_client_access pcre:/etc/postfix/filter_trusted, permit_sasl_authenticated, permit_mynetworks, reject_unauth_destination, pcre:/etc/postfix/filter_default",
409 "Apr 10 11:17:43 m01 postfix/proxymap[80]: warning: /etc/postfix/main.cf, line 806: overriding earlier entry: smtpd_client_message_rate_limit=0",
410 "Apr 10 11:17:43 m01 postfix/proxymap[80]: warning: /etc/postfix/main.cf, line 819: overriding earlier entry: smtpd_recipient_restrictions=check_client_access pcre:/etc/postfix/filter_trusted, permit_sasl_authenticated, permit_mynetworks, reject_unauth_destination, pcre:/etc/postfix/filter_default",
411 "Apr 10 11:17:43 m01 postfix/proxymap[80]: warning: /etc/postfix/main.cf, line 826: overriding earlier entry: smtpd_discard_ehlo_keywords=silent-discard,dsn,etrn",
412 ];
413
414 for (i, log_line) in real_logs.iter().enumerate() {
415 let result = parser.parse_log_line(log_line);
416 assert!(
417 result.is_ok(),
418 "Failed to parse real log sample {}: {}",
419 i,
420 log_line
421 );
422
423 let event = result.unwrap();
424 let ProxymapEventType::ConfigOverrideWarning { file_path, .. } = event.event_type;
425 assert_eq!(file_path, "/etc/postfix/main.cf");
426 }
427 }
428}