logcheck_fluent_bit_filter/cli/
args.rs

1use clap::{Parser, Subcommand, ValueEnum};
2use clap_complete::Shell;
3use std::path::PathBuf;
4
5/// Journald-specific modes
6#[cfg(target_os = "linux")]
7#[derive(Subcommand, Clone, Debug)]
8pub enum JournaldMode {
9    /// Launch interactive analyzer for unmatched entries
10    Analyze {
11        /// Minimum group size to propose pattern
12        #[arg(long, default_value = "2", help = "Minimum matches to propose pattern")]
13        min_group_size: usize,
14    },
15}
16
17/// Logcheck-based log filtering tool
18#[derive(Parser)]
19#[command(name = "logcheck-filter")]
20#[command(about = "Filter logs using logcheck rules")]
21#[command(version)]
22pub struct Cli {
23    /// Path to logcheck rules directory (defaults to /etc/logcheck)
24    #[arg(
25        long,
26        default_value = "/etc/logcheck",
27        help = "Path to logcheck rules directory"
28    )]
29    pub rules: PathBuf,
30
31    /// Output format
32    #[arg(long, value_enum, default_value = "text", help = "Output format")]
33    pub format: OutputFormat,
34
35    /// Show mode
36    #[arg(long, value_enum, default_value = "all", help = "What entries to show")]
37    pub show: ShowMode,
38
39    /// Show statistics after processing
40    #[arg(long, help = "Show processing statistics")]
41    pub stats: bool,
42
43    /// Enable colored output
44    #[arg(long, help = "Enable colored output")]
45    pub color: bool,
46
47    /// Write filtered logs to file (informational logs go to stdout)
48    #[arg(long, help = "Write filtered logs to file")]
49    pub output_file: Option<PathBuf>,
50
51    /// Generate CLI documentation in Markdown format
52    #[arg(
53        long,
54        hide = true,
55        help = "Generate CLI documentation in Markdown format"
56    )]
57    pub generate_docs: bool,
58
59    /// Generate shell completion scripts
60    #[arg(
61        long,
62        value_enum,
63        hide = true,
64        help = "Generate shell completion scripts"
65    )]
66    pub generate_completion: Option<Shell>,
67
68    /// Input source
69    #[command(subcommand)]
70    pub input: InputSource,
71}
72
73#[derive(Subcommand)]
74pub enum InputSource {
75    /// Read from a file
76    File {
77        /// Path to log file
78        path: PathBuf,
79    },
80    /// Read from standard input
81    Stdin,
82    /// Read from systemd journal
83    #[cfg(target_os = "linux")]
84    Journald {
85        /// Systemd unit to filter
86        #[arg(long, help = "Filter by systemd unit")]
87        unit: Option<String>,
88        /// Follow mode (like tail -f)
89        #[arg(long, help = "Follow new journal entries")]
90        follow: bool,
91        /// Number of lines to show from end
92        #[arg(long, help = "Show last N entries")]
93        lines: Option<usize>,
94        /// Mode for journald input
95        #[command(subcommand)]
96        mode: Option<JournaldMode>,
97    },
98}
99
100#[derive(ValueEnum, Clone, Debug)]
101pub enum OutputFormat {
102    /// Human-readable text format
103    Text,
104    /// JSON format
105    Json,
106}
107
108#[derive(ValueEnum, Clone, Debug)]
109pub enum ShowMode {
110    /// Show all log entries
111    All,
112    /// Show only violations (cracking/violations)
113    Violations,
114    /// Show only unmatched entries
115    Unmatched,
116}
117
118impl Cli {
119    /// Parse command line arguments
120    pub fn parse_args() -> Self {
121        Self::parse()
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128
129    #[test]
130    fn test_cli_parsing() {
131        // Test basic file input with explicit rules path
132        let args = vec![
133            "logcheck-filter",
134            "--rules",
135            "/etc/logcheck",
136            "file",
137            "/var/log/syslog",
138        ];
139        let cli = Cli::try_parse_from(args).unwrap();
140
141        assert_eq!(cli.rules, PathBuf::from("/etc/logcheck"));
142        assert!(matches!(cli.format, OutputFormat::Text));
143        assert!(matches!(cli.show, ShowMode::All));
144        assert!(!cli.stats);
145        assert!(!cli.color);
146        assert!(matches!(cli.input, InputSource::File { .. }));
147    }
148
149    #[test]
150    fn test_cli_default_rules() {
151        // Test that rules defaults to /etc/logcheck when not specified
152        let args = vec!["logcheck-filter", "stdin"];
153        let cli = Cli::try_parse_from(args).unwrap();
154
155        assert_eq!(cli.rules, PathBuf::from("/etc/logcheck"));
156    }
157
158    #[test]
159    fn test_cli_with_options() {
160        let args = vec![
161            "logcheck-filter",
162            "--rules",
163            "/etc/logcheck",
164            "--format",
165            "json",
166            "--show",
167            "violations",
168            "--stats",
169            "--color",
170            "stdin",
171        ];
172        let cli = Cli::try_parse_from(args).unwrap();
173
174        assert!(matches!(cli.format, OutputFormat::Json));
175        assert!(matches!(cli.show, ShowMode::Violations));
176        assert!(cli.stats);
177        assert!(cli.color);
178        assert!(matches!(cli.input, InputSource::Stdin));
179    }
180
181    #[test]
182    #[cfg(target_os = "linux")]
183    fn test_journald_options() {
184        let args = vec![
185            "logcheck-filter",
186            "--rules",
187            "/etc/logcheck",
188            "journald",
189            "--unit",
190            "sshd",
191            "--follow",
192            "--lines",
193            "100",
194        ];
195        let cli = Cli::try_parse_from(args).unwrap();
196
197        if let InputSource::Journald {
198            unit,
199            follow,
200            lines,
201            mode,
202        } = cli.input
203        {
204            assert_eq!(unit, Some("sshd".to_string()));
205            assert!(follow);
206            assert_eq!(lines, Some(100));
207            assert!(mode.is_none());
208        } else {
209            panic!("Expected Journald input source");
210        }
211    }
212
213    #[test]
214    #[cfg(target_os = "linux")]
215    fn test_journald_analyze_mode() {
216        let args = vec![
217            "logcheck-filter",
218            "journald",
219            "analyze",
220            "--min-group-size",
221            "5",
222        ];
223        let cli = Cli::try_parse_from(args).unwrap();
224
225        if let InputSource::Journald {
226            unit,
227            follow,
228            lines,
229            mode,
230        } = cli.input
231        {
232            assert_eq!(unit, None);
233            assert!(!follow);
234            assert_eq!(lines, None);
235            assert!(mode.is_some());
236
237            if let Some(JournaldMode::Analyze { min_group_size }) = mode {
238                assert_eq!(min_group_size, 5);
239            } else {
240                panic!("Expected Analyze mode");
241            }
242        } else {
243            panic!("Expected Journald input source");
244        }
245    }
246}