use crate::{Error, Result};
use std::collections::HashMap;
const LEEF_HEADERS: [&str; 6] = [
"deviceVendor",
"deviceProduct",
"deviceVersion",
"eventId",
"delimiter",
"extra_field",
];
#[derive(Clone, Debug, Default)]
struct LeefLine {
syslog_priority: Option<String>,
syslog_facility: Option<String>,
syslog_severity: Option<String>,
at: Option<String>,
ahost: Option<String>,
leef_header: HashMap<String, String>,
leef_event_attributes: String,
leef_header_event_attributes: HashMap<String, String>,
}
pub trait LeefToHashMap {
fn to_hashmap(&self, preserve_orig: bool) -> Result<HashMap<String, String>>;
}
impl LeefToHashMap for &str {
fn to_hashmap(&self, preserve_orig: bool) -> Result<HashMap<String, String>> {
leef_to_map(self, preserve_orig)
}
}
impl LeefToHashMap for String {
fn to_hashmap(&self, preserve_orig: bool) -> Result<HashMap<String, String>> {
leef_to_map(self, preserve_orig)
}
}
fn leef_to_map(leef_str: &str, preserve_orig: bool) -> Result<HashMap<String, String>> {
let parsed = parse_leef_line(leef_str)?;
let mut map = parsed.leef_header.to_owned();
if let Some(ahost) = parsed.ahost {
map.insert("ahost".to_string(), ahost);
}
if let Some(at) = parsed.at {
map.insert("at".to_string(), at);
}
if let Some(facility) = parsed.syslog_facility {
map.insert("syslog_facility".to_string(), facility);
}
if let Some(pri) = parsed.syslog_severity {
map.insert("syslog_severity".to_string(), pri);
}
if let Some(pri) = parsed.syslog_priority {
map.insert("syslog_priority".to_string(), pri);
}
if !parsed.leef_event_attributes.is_empty() {
if let Some(delim) = parsed.leef_header.get("delimiter") {
map.extend(parse_leef_event_attributes(
&parsed.leef_event_attributes,
Some(delim),
));
} else {
map.extend(parse_leef_event_attributes(
&parsed.leef_event_attributes,
None,
));
}
}
if preserve_orig {
map.insert("rawEvent".to_string(), leef_str.trim().to_string());
}
if !parsed.leef_header_event_attributes.is_empty() {
map.extend(parsed.leef_header_event_attributes)
}
map.remove("delimiter");
Ok(map)
}
fn parse_leef_line(s: &str) -> Result<LeefLine> {
if !s.to_lowercase().contains("leef:1.0|") && !s.to_lowercase().contains("leef:2.0|") {
return Err(Error::NotLeef);
}
if s.matches('|').count().lt(&5) {
return Err(Error::MalformedLeef);
}
let mut res = LeefLine::default();
let arr = s
.split("LEEF:")
.filter(|&x| !x.is_empty())
.collect::<Vec<_>>();
let header = arr
.last()
.unwrap()
.rsplitn(2, '|')
.take(2)
.collect::<Vec<_>>()[1]
.split('|')
.skip(1)
.map(|x| x.trim().to_string());
res.leef_header = LEEF_HEADERS
.into_iter()
.map(|x| x.to_string())
.zip(header.into_iter())
.collect();
res.leef_event_attributes = arr
.last()
.unwrap()
.rsplitn(2, '|')
.take(2)
.collect::<Vec<_>>()[0]
.to_string();
if let Some(delim) = res.leef_header.get("delimiter") {
if delim.contains('=') {
res.leef_header_event_attributes
.extend(convert_to_kv(delim, "="));
res.leef_header.remove("delimiter");
}
}
if let Some(extra) = res.leef_header.get("extra_field") {
if extra.contains('=') {
res.leef_header_event_attributes
.extend(convert_to_kv(extra, "="));
res.leef_header.remove("extra_field");
}
}
if arr.len().eq(&2) {
let syslog_data = arr.first().unwrap().trim();
let mut data;
if syslog_data.starts_with('<') && syslog_data.contains('>') {
let pri = &syslog_data[1..syslog_data.find('>').unwrap()];
if let Ok(parsed) = pri.parse::<i16>() {
res.syslog_facility = Some((parsed >> 3).to_string());
res.syslog_severity = Some((parsed & 7).to_string());
res.syslog_priority = Some(pri.to_string());
}
data = &syslog_data[syslog_data.find('>').unwrap() + 1..];
if data.starts_with("1 ") {
data = &data[2..];
}
} else {
data = syslog_data;
}
if data.matches(' ').count().eq(&1) {
let x = data
.rsplitn(2, ' ')
.filter(|&x| !x.is_empty())
.collect::<Vec<_>>();
if x.len().eq(&2) {
res.ahost = x.first().map(|x| x.to_string());
res.at = x.last().map(|x| x.to_string());
} else if x.len().eq(&1) {
let ss = x.first().unwrap();
if is_datetime_str(ss) {
res.at = Some(ss.to_string())
} else {
res.ahost = Some(ss.to_string())
}
}
} else if data.matches(' ').count().eq(&2) {
res.at = Some(data.to_string())
} else if data.matches(' ').count().gt(&2) {
let x = data
.rsplitn(2, ' ')
.filter(|&x| !x.is_empty())
.collect::<Vec<_>>();
res.ahost = x.first().map(|x| x.to_string());
res.at = x.last().map(|x| x.to_string());
} else if data.matches(' ').count().eq(&0) {
if is_datetime_str(data) {
res.at = Some(data.to_string())
} else {
res.ahost = Some(data.to_string())
}
}
}
Ok(res)
}
fn parse_leef_event_attributes(s: &str, delim: Option<&str>) -> HashMap<String, String> {
let mut map = HashMap::new();
if s.contains('\t') || s.contains("\\t") {
let mut attrs = s.split('\t').collect::<Vec<&str>>();
if !attrs.len().gt(&1) {
attrs = s.split("\\t").collect::<Vec<&str>>();
}
map = attrs
.iter()
.flat_map(|x| convert_to_kv(x, "="))
.collect::<HashMap<String, String>>();
} else if let Some(delim) = delim {
let attrs = s.split(delim).collect::<Vec<&str>>();
map = attrs
.iter()
.flat_map(|x| convert_to_kv(x, "="))
.collect::<HashMap<String, String>>();
} else {
let split_by_equalto = split_with_escaped(s.trim(), &'=');
let mut key = "".to_string();
for s in split_by_equalto.windows(2) {
let key_t = s[0].split(' ').collect::<Vec<&str>>();
key = key_t.last().unwrap().to_string();
let value = s[1]
.split(' ')
.collect::<Vec<&str>>()
.split_last()
.unwrap()
.1
.join(" ");
map.insert(key.clone(), value);
}
if !&key.is_empty() {
let (last, _) = split_by_equalto.split_last().unwrap();
map.insert(key, last.to_string());
}
let mut elems = vec![];
for key in map.keys() {
if key.ends_with("Label") && map.contains_key(&key[..key.len() - 5]) {
elems.push(key[..key.len() - 5].to_string());
}
}
for e in elems {
let (_, key) = map.remove_entry(&format!("{}Label", e)).unwrap();
let (_, value) = map.remove_entry(&e).unwrap();
map.insert(key.replace(' ', ""), value);
}
}
map
}
fn split_with_escaped<'a>(s: &'a str, ch: &char) -> Vec<&'a str> {
let mut res = vec![];
let mut offset = 0;
for i in 0..s.len() {
if s.as_bytes()[i] == *ch as u8 {
if i > 0 && s.as_bytes()[i - 1] == b'\\' {
continue;
}
res.push(&s[offset..i]);
offset = i + 1;
}
}
res.push(&s[offset..]);
res
}
fn convert_to_kv(s: &str, delim: &str) -> HashMap<String, String> {
let kv = s.splitn(2, delim).collect::<Vec<&str>>();
HashMap::from([(kv[0].trim().to_lowercase(), kv[1].trim().to_string())])
}
fn is_datetime_str(s: &str) -> bool {
(s.contains(':') && s.contains('-')) || s.contains('-') || s.matches(' ').count().ge(&1)
}