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#[derive(Clone, Debug)]
98pub enum Runmode {
99 Single,
100 AutoFp,
101 Workers,
102}
103
104#[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
144pub struct Config {
146 pub runmode: Runmode,
148 pub outputs: Vec<Box<dyn Output + Send + Sync>>,
150 pub enable_community_id: bool,
152 pub materialize_config_to: PathBuf,
154 pub exe_path: PathBuf,
157 pub rule_path: PathBuf,
159 pub suricata_config_path: PathBuf,
162 pub internal_ips: InternalIps,
164 pub max_pending_packets: u16,
166 pub buffer_size: Option<usize>,
168 pub default_log_dir: PathBuf,
170 pub close_grace_period: Option<Duration>,
172 pub ipc_plugin: IpcPluginConfig,
174 pub plugins: Vec<Box<dyn Plugin + Send + Sync>>,
176 pub detect_profile: DetectProfile,
178 pub async_oneside: bool,
180 pub filestore: filestore::Filestore,
182 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}