use std::time::Duration;
use flowscope::analysis::FlowAnalyzer;
use flowscope::detect::{IocKind, IocSet};
use flowscope::dns::{DnsFlags, DnsQuery, DnsQuestion};
use flowscope::emit::EveJsonWriter;
use flowscope::extract::FiveTupleKey;
use flowscope::tls::{TlsHandshake, TlsVersion};
use flowscope::{FlowStats, L4Proto, Timestamp};
fn key(dst_ip: &str, dport: u16) -> FiveTupleKey {
FiveTupleKey::new(
L4Proto::Tcp,
"10.0.0.50:51000".parse().unwrap(),
format!("{dst_ip}:{dport}").parse().unwrap(),
)
}
fn stats(pkts: u64, bytes: u64) -> FlowStats {
let mut s = FlowStats::default();
s.packets_initiator = pkts;
s.bytes_initiator = bytes;
s
}
fn main() {
let mut ioc = IocSet::with_capacity(100_000);
ioc.insert(
IocKind::Domain,
"badcdn.example",
Some(90),
Some("intel-feed"),
);
ioc.insert(IocKind::Ipv4, "198.51.100.7", Some(80), Some("c2-tracker"));
ioc.insert(
IocKind::Ja4,
"t13d1516h2_8daaf6152771_deadbeefcafe",
Some(95),
Some("malware-ja4"),
);
let mut analyzer =
FlowAnalyzer::<FiveTupleKey>::with_capacity(Duration::from_secs(120), 100_000)
.with_ioc(ioc);
let ts = Timestamp::new(1, 0);
let a = key("93.184.216.34", 443);
let mut hs_a = TlsHandshake::default();
hs_a.sni = Some("www.example.com".into());
hs_a.version = Some(TlsVersion::Tls1_0);
hs_a.cipher_suite = Some(0x0005); analyzer.observe_tls(&a, &hs_a, ts);
let b = key("198.51.100.7", 53);
let q = DnsQuery::new(
0x1234,
DnsFlags(0),
vec![DnsQuestion::new("kq3v9z7xj2wqp1.com", 1, 1)],
ts,
);
analyzer.observe_dns_query(&b, &q, ts);
let c = key("203.0.113.10", 8443);
let mut hs_c = TlsHandshake::default();
hs_c.sni = Some("cdn.badcdn.example".into()); hs_c.version = Some(TlsVersion::Tls1_3);
hs_c.ja4 = Some("t13d1516h2_8daaf6152771_deadbeefcafe".into());
analyzer.observe_tls(&c, &hs_c, ts);
let d = key("140.82.112.3", 443);
let mut hs_d = TlsHandshake::default();
hs_d.sni = Some("github.com".into());
hs_d.version = Some(TlsVersion::Tls1_3);
hs_d.cipher_suite = Some(0x1301); analyzer.observe_tls(&d, &hs_d, ts);
let flows = [
("A legacy-TLS", a, stats(20, 4_000)),
("B dga-dns ", b, stats(2, 180)),
("C bad-ja4 ", c, stats(15, 9_000)),
("D clean ", d, stats(40, 28_000)),
];
let records: Vec<_> = flows
.into_iter()
.map(|(label, k, st)| (label, analyzer.finalize(&k, st)))
.collect();
assert_eq!(analyzer.len(), 0);
println!("flow clean? sev score risk / IOC");
println!("{}", "─".repeat(72));
for (label, rec) in &records {
let sev = rec.severity().map(|s| s.as_str()).unwrap_or("-");
let risks: Vec<&str> = rec.risk.as_slugs().collect();
let mut detail = risks.join(",");
for m in &rec.ioc_hits {
detail.push_str(&format!(
" ⚑ioc:{}={}{}",
m.kind.as_str(),
m.value,
m.source
.as_deref()
.map(|s| format!("({s})"))
.unwrap_or_default()
));
}
if detail.is_empty() {
detail.push('-');
}
println!(
"{label} {:>5} {sev:<6} {:>4} {detail}",
if rec.is_clean() { "yes" } else { "NO" },
rec.score(),
);
}
println!("\n── EVE JSON (non-clean flows) ──");
let mut eve = EveJsonWriter::new(std::io::stdout().lock());
for (_, rec) in &records {
if !rec.is_clean() {
eve.write_analyzed_flow(rec).unwrap();
}
}
}