use std::sync::Arc;
use flowscope::PacketView;
use crate::error::Error;
use crate::protocol::FlowKey;
#[derive(Clone)]
pub struct YaraRules(Arc<yara_x::Rules>);
impl YaraRules {
pub fn compile(source: &str) -> Result<Self, Error> {
let rules =
yara_x::compile(source).map_err(|e| Error::Config(format!("YARA compile: {e}")))?;
Ok(Self(Arc::new(rules)))
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct YaraMatch {
pub rule: String,
pub namespace: String,
pub direction: ScanDirection,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ScanDirection {
Initiator,
Responder,
}
pub(crate) type YaraHandler = Box<dyn FnMut(&FlowKey, &YaraMatch) + Send>;
#[derive(Default)]
struct FlowBufs {
init: Vec<u8>,
resp: Vec<u8>,
}
pub(crate) struct YaraAccumulator {
rules: Arc<yara_x::Rules>,
extractor: flowscope::extract::FiveTuple,
flows: rustc_hash::FxHashMap<FlowKey, FlowBufs>,
handlers: Vec<YaraHandler>,
max_flows: usize,
max_bytes: usize,
}
impl YaraAccumulator {
pub(crate) fn new(
rules: YaraRules,
handlers: Vec<YaraHandler>,
max_flows: usize,
max_bytes: usize,
) -> Self {
Self {
rules: rules.0,
extractor: flowscope::extract::FiveTuple::bidirectional(),
flows: rustc_hash::FxHashMap::default(),
handlers,
max_flows,
max_bytes,
}
}
fn append_capped(buf: &mut Vec<u8>, payload: &[u8], max_bytes: usize) {
if buf.len() >= max_bytes {
return;
}
let room = max_bytes - buf.len();
let take = payload.len().min(room);
buf.extend_from_slice(&payload[..take]);
}
fn scan_dir(
rules: &yara_x::Rules,
handlers: &mut [YaraHandler],
key: &FlowKey,
buf: &[u8],
direction: ScanDirection,
) {
if buf.is_empty() {
return;
}
let mut scanner = yara_x::Scanner::new(rules);
let Ok(results) = scanner.scan(buf) else {
return;
};
for rule in results.matching_rules() {
let m = YaraMatch {
rule: rule.identifier().to_string(),
namespace: rule.namespace().to_string(),
direction,
};
for handler in handlers.iter_mut() {
handler(key, &m);
}
}
}
}
impl crate::monitor::nprint::FlowByteAccumulator for YaraAccumulator {
fn feed(&mut self, view: &PacketView<'_>) {
use flowscope::FlowExtractor;
let Some(extracted) = self.extractor.extract(*view) else {
return;
};
let Some(payload) = l4_payload(view.frame) else {
return;
};
if payload.is_empty() {
return;
}
let max_bytes = self.max_bytes;
let bufs = match self.flows.get_mut(&extracted.key) {
Some(b) => b,
None => {
if self.flows.len() >= self.max_flows {
return;
}
self.flows.entry(extracted.key).or_default()
}
};
match extracted.orientation {
flowscope::Orientation::Forward => {
Self::append_capped(&mut bufs.init, payload, max_bytes)
}
flowscope::Orientation::Reverse => {
Self::append_capped(&mut bufs.resp, payload, max_bytes)
}
}
}
fn flush(&mut self, key: &FlowKey) {
if let Some(bufs) = self.flows.remove(key) {
Self::scan_dir(
&self.rules,
&mut self.handlers,
key,
&bufs.init,
ScanDirection::Initiator,
);
Self::scan_dir(
&self.rules,
&mut self.handlers,
key,
&bufs.resp,
ScanDirection::Responder,
);
}
}
}
fn l4_payload(frame: &[u8]) -> Option<&[u8]> {
use etherparse::{SlicedPacket, TransportSlice};
let sliced = SlicedPacket::from_ethernet(frame).ok()?;
match sliced.transport? {
TransportSlice::Tcp(t) => Some(t.payload()),
TransportSlice::Udp(u) => Some(u.payload()),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn compile_rejects_bad_rules() {
assert!(YaraRules::compile("rule { this is not yara }").is_err());
}
#[test]
fn compile_accepts_a_simple_rule() {
let r =
YaraRules::compile(r#"rule eicar { strings: $a = "EICAR-STANDARD" condition: $a }"#);
assert!(r.is_ok(), "{:?}", r.err());
}
#[test]
fn append_capped_bounds_the_buffer() {
let mut buf = Vec::new();
YaraAccumulator::append_capped(&mut buf, &[1, 2, 3, 4, 5], 3);
assert_eq!(buf, vec![1, 2, 3]);
YaraAccumulator::append_capped(&mut buf, &[6, 7], 3);
assert_eq!(buf, vec![1, 2, 3], "no growth past the cap");
}
}