suricata_ipc/config/
mod.rs

1pub mod eve;
2pub mod filestore;
3pub mod ipc_plugin;
4pub mod output;
5pub mod plugin;
6
7use crate::errors::Error;
8use askama::Template;
9use ipc_plugin::{IpcPlugin, IpcPluginConfig};
10use log::debug;
11use output::Output;
12use plugin::Plugin;
13use std::io::Write;
14use std::path::PathBuf;
15use std::time::Duration;
16
17pub struct InternalIps(Vec<String>);
18
19impl InternalIps {
20    pub fn new(ips: Vec<String>) -> Self {
21        InternalIps(ips)
22    }
23}
24
25impl std::fmt::Display for InternalIps {
26    fn fmt(&self, fmt: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
27        let ips = &self.0;
28        write!(fmt, "{}", ips.join(","))?;
29        Ok(())
30    }
31}
32
33struct RenderedOutput {
34    connection: String,
35    types: String,
36}
37
38struct RenderedIpcPlugin<'a> {
39    path: std::borrow::Cow<'a, str>,
40    config: String,
41}
42
43struct RenderedPlugin<'a> {
44    path: std::borrow::Cow<'a, str>,
45    config: String,
46}
47
48#[derive(Clone)]
49pub enum AdditionalConfig {
50    String(String),
51    IncludePath(PathBuf),
52}
53
54impl AdditionalConfig {
55    pub fn check(&self) -> Result<(), Error> {
56        match self {
57            AdditionalConfig::String(_) => Ok(()),
58            AdditionalConfig::IncludePath(ref path) => {
59                if path.exists() {
60                    return Ok(());
61                }
62                return Err(Error::MissingInclude);
63            }
64        }
65    }
66}
67
68impl std::fmt::Display for AdditionalConfig {
69    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70        match self {
71            Self::String(s) => write!(f, "{}\n", s),
72            Self::IncludePath(path) => write!(f, "include: {:?}\n", path.as_path()),
73        }
74    }
75}
76
77#[derive(Template)]
78#[template(path = "suricata.yaml.in", escape = "none")]
79struct ConfigTemplate<'a> {
80    runmode: Runmode,
81    rules: &'a str,
82    outputs: Vec<RenderedOutput>,
83    community_id: &'a str,
84    suricata_config_path: &'a str,
85    internal_ips: &'a InternalIps,
86    max_pending_packets: &'a str,
87    default_log_dir: std::borrow::Cow<'a, str>,
88    ipc_plugin: RenderedIpcPlugin<'a>,
89    plugins: Vec<RenderedPlugin<'a>>,
90    detect_profile: DetectProfile,
91    async_oneside: bool,
92    filestore: &'a str,
93    additional_configs: Vec<AdditionalConfig>,
94}
95
96/// Runmodes for suricata
97#[derive(Clone, Debug)]
98pub enum Runmode {
99    Single,
100    AutoFp,
101    Workers,
102}
103
104/// Detect Profiles
105#[derive(Clone, Debug)]
106pub enum DetectProfile {
107    Low,
108    Medium,
109    High,
110}
111
112impl Default for DetectProfile {
113    fn default() -> Self {
114        Self::Medium
115    }
116}
117
118impl std::fmt::Display for DetectProfile {
119    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
120        match self {
121            Self::Low => write!(f, "low"),
122            Self::Medium => write!(f, "medium"),
123            Self::High => write!(f, "high"),
124        }
125    }
126}
127
128impl Default for Runmode {
129    fn default() -> Self {
130        Self::AutoFp
131    }
132}
133
134impl std::fmt::Display for Runmode {
135    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
136        match self {
137            Self::Single => write!(f, "single"),
138            Self::AutoFp => write!(f, "autofp"),
139            Self::Workers => write!(f, "workers"),
140        }
141    }
142}
143
144/// Configuration options for suricata
145pub struct Config {
146    /// Runmode to use
147    pub runmode: Runmode,
148    /// Outputs to connect to suricata
149    pub outputs: Vec<Box<dyn Output + Send + Sync>>,
150    /// Whether community id should be enabled, defaults to true
151    pub enable_community_id: bool,
152    /// Path where config will be materialized to
153    pub materialize_config_to: PathBuf,
154    /// Path where the suricata executable lives, defaults to /usr/bin/suricata, can be overridden
155    /// with environment variable SURICATA_EXE
156    pub exe_path: PathBuf,
157    /// Path where the rules reside at
158    pub rule_path: PathBuf,
159    /// Path where suricata config resides at (e.g. threshold config), defaults to /etc/suricata,
160    /// can be overridden with environment variable SURICATA_CONFIG_DIR
161    pub suricata_config_path: PathBuf,
162    /// Internal ips to use for HOME_NET
163    pub internal_ips: InternalIps,
164    /// Max pending packets before suricata will block on incoming packets
165    pub max_pending_packets: u16,
166    /// Adjust uds buffer size
167    pub buffer_size: Option<usize>,
168    /// Directory to use for suricata logging
169    pub default_log_dir: PathBuf,
170    /// Allowed duration before killing suricata process (defaults to None preserve previous behavior)
171    pub close_grace_period: Option<Duration>,
172    /// IPC plugin
173    pub ipc_plugin: IpcPluginConfig,
174    /// Plugins to attempt to load
175    pub plugins: Vec<Box<dyn Plugin + Send + Sync>>,
176    /// Detect profile
177    pub detect_profile: DetectProfile,
178    /// async-oneside flow handling
179    pub async_oneside: bool,
180    /// filestore configuration
181    pub filestore: filestore::Filestore,
182    /// Additional configs, allow raw string or `include` to be appended to the suricata.yaml
183    pub additional_configs: Vec<AdditionalConfig>,
184}
185
186impl Default for Config {
187    fn default() -> Self {
188        let log_dir = if let Ok(s) = std::env::var("SURICATA_LOG_DIR") {
189            PathBuf::from(s)
190        } else {
191            PathBuf::from("/var/log/suricata")
192        };
193        let suricata_config_path =
194            if let Some(e) = std::env::var_os("SURICATA_CONFIG_DIR").map(|s| PathBuf::from(s)) {
195                e
196            } else {
197                PathBuf::from("/etc/suricata")
198            };
199        Config {
200            runmode: Runmode::AutoFp,
201            outputs: vec![
202                Box::new(output::Alert::new(eve::EveConfiguration::uds(
203                    log_dir.join("alert.socket"),
204                ))),
205                Box::new(output::Flow::new(eve::EveConfiguration::uds(
206                    log_dir.join("flow.socket"),
207                ))),
208                Box::new(output::Http::new(eve::EveConfiguration::uds(
209                    log_dir.join("http.socket"),
210                ))),
211                Box::new(output::Dns::new(eve::EveConfiguration::uds(
212                    log_dir.join("dns.socket"),
213                ))),
214                Box::new(output::Stats::new(eve::EveConfiguration::uds(
215                    log_dir.join("stats.socket"),
216                ))),
217            ],
218            enable_community_id: true,
219            materialize_config_to: suricata_config_path.join("suricata-rs.yaml"),
220            exe_path: {
221                if let Some(e) = std::env::var_os("SURICATA_EXE").map(PathBuf::from) {
222                    e
223                } else {
224                    PathBuf::from("/usr/local/bin/suricata")
225                }
226            },
227            rule_path: PathBuf::from("/etc/suricata/custom.rules"),
228            suricata_config_path: suricata_config_path,
229            internal_ips: InternalIps(vec![
230                String::from("10.0.0.0/8,172.16.0.0/12"),
231                String::from("e80:0:0:0:0:0:0:0/64"),
232                String::from("127.0.0.1/32"),
233                String::from("fc00:0:0:0:0:0:0:0/7"),
234                String::from("192.168.0.0/16"),
235                String::from("169.254.0.0/16"),
236            ]),
237            max_pending_packets: 2_500,
238            buffer_size: None,
239            default_log_dir: log_dir,
240            ipc_plugin: IpcPluginConfig::default(),
241            plugins: vec![],
242            close_grace_period: None,
243            detect_profile: DetectProfile::Medium,
244            async_oneside: false,
245            filestore: filestore::Filestore::default(),
246            additional_configs: vec![],
247        }
248    }
249}
250
251impl Config {
252    fn render<'a>(&'a self, ipc_plugin: IpcPlugin) -> Result<String, Error> {
253        let rules = self.rule_path.to_string_lossy().to_owned();
254        let suricata_config_path = self.suricata_config_path.to_string_lossy().to_owned();
255        let internal_ips = &self.internal_ips;
256        let community_id = if self.enable_community_id {
257            "yes"
258        } else {
259            "no"
260        };
261        let default_log_dir = self.default_log_dir.to_string_lossy();
262        let max_pending_packets = format!("{}", self.max_pending_packets);
263        let outputs = self
264            .outputs
265            .iter()
266            .map(|o| RenderedOutput {
267                connection: o.eve().render(&o.output_type()),
268                types: o.render_messages(),
269            })
270            .collect();
271        let plugins = self
272            .plugins
273            .iter()
274            .map(|p| RenderedPlugin {
275                path: p.path().to_string_lossy(),
276                config: p.config().unwrap_or_else(|| "".into()),
277            })
278            .collect();
279        let filestore = self.filestore.render(&self.default_log_dir)?;
280
281        self.additional_configs
282            .iter()
283            .map(|c| c.check())
284            .collect::<Result<(), Error>>()?;
285
286        let template = ConfigTemplate {
287            runmode: self.runmode.clone(),
288            rules: &rules,
289            community_id: &community_id,
290            suricata_config_path: &suricata_config_path,
291            internal_ips: internal_ips,
292            max_pending_packets: &max_pending_packets,
293            default_log_dir: default_log_dir,
294            outputs: outputs,
295            ipc_plugin: RenderedIpcPlugin {
296                path: ipc_plugin.path.to_string_lossy(),
297                config: ipc_plugin.render().unwrap(),
298            },
299            plugins: plugins,
300            detect_profile: self.detect_profile.clone(),
301            async_oneside: self.async_oneside,
302            filestore: &filestore,
303            additional_configs: self.additional_configs.clone(),
304        };
305
306        debug!("Attempting to render");
307        template.render().map_err(Error::from)
308    }
309
310    pub fn materialize(&self, ipc_plugin: IpcPlugin) -> Result<(), Error> {
311        let rendered = self.render(ipc_plugin)?;
312        debug!("Writing output.yaml to {:?}", self.materialize_config_to);
313        let mut f = std::fs::File::create(&self.materialize_config_to).map_err(Error::Io)?;
314        f.write(rendered.as_bytes()).map_err(Error::from)?;
315        debug!("Output file written");
316        Ok(())
317    }
318}
319
320#[cfg(test)]
321mod tests {
322    use super::*;
323
324    use crate::config::output::OutputType;
325    use crate::config::InternalIps;
326    use tempfile::NamedTempFile;
327
328    #[test]
329    fn test_internal_ip_display() {
330        let internal_ips = InternalIps(vec![
331            "169.254.0.0/16".to_owned(),
332            "192.168.0.0/16".to_owned(),
333            "fc00:0:0:0:0:0:0:0/7".to_owned(),
334            "127.0.0.1/32".to_owned(),
335            "10.0.0.0/8".to_owned(),
336            "172.16.0.0/12".to_owned(),
337        ]);
338        assert_eq!(format!("{}", internal_ips), "169.254.0.0/16,192.168.0.0/16,fc00:0:0:0:0:0:0:0/7,127.0.0.1/32,10.0.0.0/8,172.16.0.0/12");
339    }
340
341    fn ipc_plugin() -> IpcPlugin {
342        let cfg = IpcPluginConfig {
343            ipc_to_suricata_channel_size: 1,
344            path: PathBuf::from("ipc-plugin.so"),
345            allocation_batch_size: 100,
346            servers: 1,
347            live: true,
348        };
349        let (plugin, _) = cfg.into_plugin().unwrap();
350        plugin
351    }
352
353    #[test]
354    fn test_alert_redis() {
355        let eve_config = || {
356            eve::EveConfiguration::Redis(eve::Redis {
357                server: "redis://test".into(),
358                port: 6379,
359            })
360        };
361        let outputs: Vec<Box<dyn output::Output + Send + Sync>> =
362            vec![Box::new(output::Alert::new(eve_config()))];
363        let mut cfg = Config::default();
364        cfg.outputs = outputs;
365        let rendered = cfg.render(ipc_plugin()).unwrap();
366
367        let regex = regex::Regex::new(
368            r#"filetype: redis\s*[\r\n]\s*redis:\s*[\r\n](.*[\r\n])*\s*types:\s*[\r\n]\s*- alert"#,
369        )
370        .unwrap();
371
372        assert!(regex.find(&rendered).is_some());
373    }
374
375    #[test]
376    fn test_alert_uds() {
377        let eve_config = || eve::EveConfiguration::uds(PathBuf::from("test.socket"));
378        let outputs: Vec<Box<dyn output::Output + Send + Sync>> =
379            vec![Box::new(output::Alert::new(eve_config()))];
380        let mut cfg = Config::default();
381        cfg.outputs = outputs;
382        let rendered = cfg.render(ipc_plugin()).unwrap();
383
384        let regex = regex::Regex::new(
385            r#"filetype:\s+unix_stream\s*[\r\n](.*[\r\n])*\s*types:\s*[\r\n]*\s*- alert"#,
386        )
387        .unwrap();
388
389        assert!(regex.find(&rendered).is_some());
390    }
391
392    struct Custom {
393        uds: Option<PathBuf>,
394    }
395
396    impl eve::Custom for Custom {
397        fn name(&self) -> &str {
398            "custom"
399        }
400        fn options(&self, _output_type: &OutputType) -> std::collections::HashMap<String, String> {
401            let mut m = std::collections::HashMap::default();
402            m.insert("test-name".into(), "test-key".into());
403            m
404        }
405        fn listener(&self, _output_type: &OutputType) -> Option<PathBuf> {
406            self.uds.clone()
407        }
408        fn render(&self, output_type: &OutputType) -> String {
409            eve::render_custom(self, output_type)
410        }
411    }
412
413    #[test]
414    fn test_alert_custom_non_uds() {
415        let eve_config = || eve::EveConfiguration::Custom(Box::new(Custom { uds: None }));
416        let outputs: Vec<Box<dyn output::Output + Send + Sync>> =
417            vec![Box::new(output::Alert::new(eve_config()))];
418        let mut cfg = Config::default();
419        cfg.outputs = outputs;
420        let rendered = cfg.render(ipc_plugin()).unwrap();
421
422        let regex = regex::Regex::new(r#"filetype:\s+custom\s*[\r\n]\s*custom:\s*[\r\n]*\s*test-name: test-key\s*[\r\n](.*[\r\n])*\s*types:\s*[\r\n]*\s*- alert"#).unwrap();
423
424        assert!(regex.find(&rendered).is_some());
425    }
426
427    #[test]
428    fn test_alert_custom_uds() {
429        let eve_config = || {
430            eve::EveConfiguration::Custom(Box::new(Custom {
431                uds: Some(PathBuf::from("test.path")),
432            }))
433        };
434        let outputs: Vec<Box<dyn output::Output + Send + Sync>> =
435            vec![Box::new(output::Alert::new(eve_config()))];
436        let mut cfg = Config::default();
437        cfg.outputs = outputs;
438        let rendered = cfg.render(ipc_plugin()).unwrap();
439
440        let regex = regex::Regex::new(r#"filetype:\s+custom\s*[\r\n]\s*custom:\s*[\r\n]\s*filename:\s+test.path\s*[\r\n]\s*test-name: test-key\s*[\r\n](.*[\r\n])*\s*types:\s*[\r\n]*\s*- alert"#).unwrap();
441
442        assert!(regex.find(&rendered).is_some());
443    }
444
445    #[test]
446    fn test_dns_uds() {
447        let eve_config = || eve::EveConfiguration::uds(PathBuf::from("test.socket"));
448        let outputs: Vec<Box<dyn output::Output + Send + Sync>> =
449            vec![Box::new(output::Dns::new(eve_config()))];
450        let mut cfg = Config::default();
451        cfg.outputs = outputs;
452        let rendered = cfg.render(ipc_plugin()).unwrap();
453
454        let regex = regex::Regex::new(r#"filetype:\s+unix_stream\s*[\r\n]\s*filename: test\.socket.Dns\.socket\s*[\r\n](.*[\r\n])*\s*types:\s*[\r\n]*\s*- dns"#).unwrap();
455
456        assert!(regex.find(&rendered).is_some());
457    }
458
459    #[test]
460    fn test_default_http() {
461        let eve_config = || eve::EveConfiguration::uds(PathBuf::from("test.socket"));
462        let outputs: Vec<Box<dyn output::Output + Send + Sync>> =
463            vec![Box::new(output::Http::new(eve_config()))];
464        let mut cfg = Config::default();
465        cfg.outputs = outputs;
466        let rendered = cfg.render(ipc_plugin()).unwrap();
467
468        let regex = regex::Regex::new(r#"filetype:\s+unix_stream\s*[\r\n]\s*filename: test\.socket.Http\.socket\s*[\r\n](.*[\r\n])*\s*types:\s*[\r\n]*\s*- http:"#).unwrap();
469
470        assert!(regex.find(&rendered).is_some());
471    }
472
473    #[test]
474    fn test_custom_http() {
475        let eve_config = || eve::EveConfiguration::uds(PathBuf::from("test.socket"));
476        let mut http = output::Http::new(eve_config());
477        http.extended = true;
478        http.custom = vec!["Accept-Encoding".to_string()];
479        let outputs: Vec<Box<dyn output::Output + Send + Sync>> = vec![Box::new(http)];
480        let mut cfg = Config::default();
481        cfg.outputs = outputs;
482        let rendered = cfg.render(ipc_plugin()).unwrap();
483
484        let regex = regex::Regex::new(r#"filetype:\s+unix_stream\s*[\r\n]\s*filename: test.socket.Http.socket\s*[\r\n](.*[\r\n])*\s*types:\s*[\r\n]*\s*- http:\s*(.*[\r\n])*\s*extended: yes\s*(.*[\r\n])*\s*custom: \[Accept\-Encoding\]"#).unwrap();
485
486        assert!(regex.find(&rendered).is_some());
487    }
488
489    #[test]
490    fn test_render_additional_includes_string() {
491        let eve_config = || eve::EveConfiguration::uds(PathBuf::from("test.socket"));
492        let mut http = output::Http::new(eve_config());
493        http.extended = true;
494        http.custom = vec!["Accept-Encoding".to_string()];
495        let _outputs: Vec<Box<dyn output::Output + Send + Sync>> = vec![Box::new(http)];
496        let mut cfg = Config::default();
497        cfg.additional_configs = vec![
498            AdditionalConfig::String(String::from("some:\n  random::config")),
499            AdditionalConfig::String(String::from("has_a_newline: true")),
500        ];
501        let rendered = cfg.render(ipc_plugin()).unwrap();
502
503        let first_match = "some:\n  random::config";
504        let second_match = "has_a_newline: true";
505
506        assert!(rendered.find(&first_match).is_some());
507        assert!(rendered.find(&second_match).is_some());
508    }
509    #[test]
510    fn test_render_additional_includes_found_file() {
511        let eve_config = || eve::EveConfiguration::uds(PathBuf::from("test.socket"));
512        let mut http = output::Http::new(eve_config());
513        http.extended = true;
514        http.custom = vec!["Accept-Encoding".to_string()];
515        let _outputs: Vec<Box<dyn output::Output + Send + Sync>> = vec![Box::new(http)];
516        let mut cfg = Config::default();
517        let tempfile = NamedTempFile::new().unwrap();
518        let existes = PathBuf::from(tempfile.path());
519        cfg.additional_configs = vec![AdditionalConfig::IncludePath(existes)];
520        let rendered = cfg.render(ipc_plugin()).unwrap();
521
522        let mat = format!("include: {:?}", tempfile.path());
523
524        assert!(rendered.find(&mat).is_some());
525    }
526
527    #[test]
528    fn test_render_additional_includes_missing_file() {
529        let eve_config = || eve::EveConfiguration::uds(PathBuf::from("test.socket"));
530        let mut http = output::Http::new(eve_config());
531        http.extended = true;
532        http.custom = vec!["Accept-Encoding".to_string()];
533        let _outputs: Vec<Box<dyn output::Output + Send + Sync>> = vec![Box::new(http)];
534        let mut cfg = Config::default();
535        let missing = PathBuf::from("/nothere");
536        cfg.additional_configs = vec![AdditionalConfig::IncludePath(missing)];
537        let rendered = cfg.render(ipc_plugin());
538        match rendered {
539            Err(Error::MissingInclude) => {}
540            _ => panic!("Wrong or missing error for include"),
541        }
542    }
543}