postfix_log_parser/components/
postfix_script.rs

1//! # Postfix Script 系统脚本组件解析器
2//!
3//! Postfix Script 是 Postfix 的系统管理脚本组件,负责:
4//! - 执行系统启动、停止、重载操作
5//! - 验证 Postfix 配置文件和权限
6//! - 检查系统目录的安全性和所有权
7//! - 处理管理命令的执行和错误报告
8//!
9//! ## 核心功能
10//!
11//! - **系统操作**: 启动、运行状态检查、配置重载
12//! - **安全检查**: 文件权限、目录所有权验证
13//! - **命令执行**: postconf 等系统命令的执行
14//! - **错误处理**: 致命错误和警告的记录
15//!
16//! ## 支持的事件类型
17//!
18//! - **系统操作**: 启动、运行、刷新配置等操作
19//! - **致命错误**: 命令执行失败、配置错误
20//! - **警告事件**: 权限问题、安全风险警告
21//!
22//! ## 示例日志格式
23//!
24//! ```text
25//! # 系统操作
26//! starting the Postfix mail system
27//! the Postfix mail system is running: PID: 12345
28//! refreshing the Postfix mail system
29//!
30//! # 致命错误
31//! cannot execute /usr/sbin/postconf!
32//!
33//! # 警告事件
34//! not owned by root: /etc/postfix/main.cf
35//! group or other writable: /var/spool/postfix
36//! symlink leaves directory: /etc/postfix/aliases
37//! ```
38
39use crate::components::ComponentParser;
40use crate::error::ParseError;
41use crate::events::base::{BaseEvent, PostfixLogLevel};
42use crate::events::postfix_script::{
43    PostfixScriptEvent, PostfixScriptFatalError, PostfixScriptOperation, PostfixScriptWarningType,
44};
45use crate::events::ComponentEvent;
46use chrono::{DateTime, Utc};
47use regex::Regex;
48
49pub struct PostfixScriptParser {
50    // System operations
51    starting_regex: Regex,
52    running_regex: Regex,
53    refreshing_regex: Regex,
54
55    // Fatal errors
56    cannot_execute_postconf_regex: Regex,
57    cannot_execute_command_regex: Regex,
58
59    // Warnings
60    not_owned_regex: Regex,
61    group_writable_regex: Regex,
62    symlink_leaves_regex: Regex,
63}
64
65impl PostfixScriptParser {
66    pub fn new() -> Self {
67        PostfixScriptParser {
68            // System operations
69            starting_regex: Regex::new(r"^starting the Postfix mail system\s*$").unwrap(),
70            running_regex: Regex::new(r"^the Postfix mail system is running: PID: (\d+)\s*$")
71                .unwrap(),
72            refreshing_regex: Regex::new(r"^refreshing the Postfix mail system\s*$").unwrap(),
73
74            // Fatal errors
75            cannot_execute_postconf_regex: Regex::new(r"^cannot execute /usr/sbin/postconf!$")
76                .unwrap(),
77            cannot_execute_command_regex: Regex::new(r"^cannot execute (.+?)!?$").unwrap(),
78
79            // Warnings
80            not_owned_regex: Regex::new(r"^not owned by (root|postfix): (.+)$").unwrap(),
81            group_writable_regex: Regex::new(r"^group or other writable: (.+)$").unwrap(),
82            symlink_leaves_regex: Regex::new(r"^symlink leaves directory: (.+)$").unwrap(),
83        }
84    }
85
86    fn create_base_event(&self, message: &str) -> BaseEvent {
87        BaseEvent {
88            timestamp: DateTime::parse_from_rfc3339("2024-04-27T16:20:48Z")
89                .unwrap()
90                .with_timezone(&Utc),
91            hostname: "unknown".to_string(),
92            component: "postfix-script".to_string(),
93            process_id: 0,
94            log_level: PostfixLogLevel::Info,
95            raw_message: message.to_string(),
96        }
97    }
98
99    fn parse_system_operation(&self, message: &str) -> Option<PostfixScriptEvent> {
100        let base = self.create_base_event(message);
101
102        if self.starting_regex.is_match(message) {
103            return Some(PostfixScriptEvent::SystemOperation {
104                base,
105                operation: PostfixScriptOperation::Starting,
106            });
107        }
108
109        if let Some(caps) = self.running_regex.captures(message) {
110            let pid = caps.get(1).and_then(|m| m.as_str().parse::<u32>().ok());
111            return Some(PostfixScriptEvent::SystemOperation {
112                base,
113                operation: PostfixScriptOperation::Running { pid },
114            });
115        }
116
117        if self.refreshing_regex.is_match(message) {
118            return Some(PostfixScriptEvent::SystemOperation {
119                base,
120                operation: PostfixScriptOperation::Refreshing,
121            });
122        }
123
124        None
125    }
126
127    fn parse_fatal_error(&self, message: &str) -> Option<PostfixScriptEvent> {
128        let mut base = self.create_base_event(message);
129        base.log_level = PostfixLogLevel::Fatal;
130
131        if self.cannot_execute_postconf_regex.is_match(message) {
132            return Some(PostfixScriptEvent::FatalError {
133                base,
134                error: PostfixScriptFatalError::CannotExecutePostconf,
135            });
136        }
137
138        if let Some(caps) = self.cannot_execute_command_regex.captures(message) {
139            let command = caps
140                .get(1)
141                .map(|m| m.as_str().to_string())
142                .unwrap_or_else(|| "unknown".to_string());
143            return Some(PostfixScriptEvent::FatalError {
144                base,
145                error: PostfixScriptFatalError::CannotExecuteCommand { command },
146            });
147        }
148
149        // Generic fatal error
150        Some(PostfixScriptEvent::FatalError {
151            base,
152            error: PostfixScriptFatalError::Other {
153                message: message.to_string(),
154            },
155        })
156    }
157
158    fn parse_warning(&self, message: &str) -> Option<PostfixScriptEvent> {
159        let mut base = self.create_base_event(message);
160        base.log_level = PostfixLogLevel::Warning;
161
162        if let Some(caps) = self.not_owned_regex.captures(message) {
163            let expected_owner = caps
164                .get(1)
165                .map(|m| m.as_str().to_string())
166                .unwrap_or_else(|| "unknown".to_string());
167            let path = caps
168                .get(2)
169                .map(|m| m.as_str().to_string())
170                .unwrap_or_else(|| "unknown".to_string());
171            return Some(PostfixScriptEvent::Warning {
172                base,
173                warning: PostfixScriptWarningType::NotOwnedBy {
174                    path,
175                    expected_owner,
176                },
177            });
178        }
179
180        if let Some(caps) = self.group_writable_regex.captures(message) {
181            let path = caps
182                .get(1)
183                .map(|m| m.as_str().to_string())
184                .unwrap_or_else(|| "unknown".to_string());
185            return Some(PostfixScriptEvent::Warning {
186                base,
187                warning: PostfixScriptWarningType::GroupWritable { path },
188            });
189        }
190
191        if let Some(caps) = self.symlink_leaves_regex.captures(message) {
192            let path = caps
193                .get(1)
194                .map(|m| m.as_str().to_string())
195                .unwrap_or_else(|| "unknown".to_string());
196            return Some(PostfixScriptEvent::Warning {
197                base,
198                warning: PostfixScriptWarningType::SymlinkLeaves { path },
199            });
200        }
201
202        // Generic warning
203        Some(PostfixScriptEvent::Warning {
204            base,
205            warning: PostfixScriptWarningType::Other {
206                message: message.to_string(),
207            },
208        })
209    }
210}
211
212impl ComponentParser for PostfixScriptParser {
213    fn parse(&self, message: &str) -> Result<ComponentEvent, ParseError> {
214        // Try to detect log level from message content
215        if message.contains("fatal:") || message.contains("cannot execute") {
216            if let Some(event) = self.parse_fatal_error(message) {
217                return Ok(ComponentEvent::PostfixScript(event));
218            }
219        }
220
221        if message.contains("warning:")
222            || message.contains("not owned by")
223            || message.contains("group or other writable")
224        {
225            if let Some(event) = self.parse_warning(message) {
226                return Ok(ComponentEvent::PostfixScript(event));
227            }
228        }
229
230        // Try system operations
231        if let Some(event) = self.parse_system_operation(message) {
232            return Ok(ComponentEvent::PostfixScript(event));
233        }
234
235        Err(ParseError::ComponentParseError {
236            component: "postfix-script".to_string(),
237            reason: format!("Unsupported message format: {}", message),
238        })
239    }
240
241    fn component_name(&self) -> &'static str {
242        "postfix-script"
243    }
244
245    fn can_parse(&self, component: &str) -> bool {
246        component == "postfix-script"
247    }
248}
249
250impl Default for PostfixScriptParser {
251    fn default() -> Self {
252        Self::new()
253    }
254}