use clap::Parser; use futures::TryStreamExt; use log::{debug, error, info, warn}; use pcap_file_tokio::pcapng::{Block, PcapNgReader}; use rayhunter::{ analysis::analyzer::{AnalysisRow, AnalyzerConfig, EventType, Harness}, diag::DataType, gsmtap_parser, pcap::GsmtapPcapWriter, qmdl::QmdlReader, }; use std::{collections::HashMap, future, path::PathBuf, pin::pin}; use tokio::fs::File; use walkdir::WalkDir; #[derive(Parser, Debug)] #[command(version, about)] struct Args { #[arg(short = 'p', long)] path: PathBuf, #[arg(short = 'P', long)] pcapify: bool, #[arg(long)] show_skipped: bool, #[arg(short, long)] quiet: bool, #[arg(short, long)] debug: bool, } #[derive(Default)] struct Report { skipped_reasons: HashMap, total_messages: u32, warnings: u32, skipped: u32, file_path: String, } impl Report { fn new(file_path: &str) -> Self { Report { file_path: file_path.to_string(), ..Default::default() } } fn process_row(&mut self, row: AnalysisRow) { self.total_messages += 1; if let Some(reason) = row.skipped_message_reason { *self.skipped_reasons.entry(reason).or_insert(0) += 1; self.skipped += 1; return; } for maybe_event in row.events { let Some(event) = maybe_event else { continue }; let Some(timestamp) = row.packet_timestamp else { continue; }; match event.event_type { EventType::Informational => { info!("{}: INFO - {} {}", self.file_path, timestamp, event.message,); } EventType::Low | EventType::Medium | EventType::High => { warn!( "{}: WARNING (Severity: {:?}) - {} {}", self.file_path, event.event_type, timestamp, event.message, ); self.warnings += 1; } } } } fn print_summary(&self, show_skipped: bool) { if show_skipped && self.skipped > 0 { info!("{}: messages skipped:", self.file_path); for (reason, count) in self.skipped_reasons.iter() { info!(" - {count}: \"{reason}\""); } } info!( "{}: {} messages analyzed, {} warnings, {} messages skipped", self.file_path, self.total_messages, self.warnings, self.skipped ); } } async fn analyze_pcap(pcap_path: &str, show_skipped: bool) { let mut harness = Harness::new_with_config(&AnalyzerConfig::default()); let pcap_file = &mut File::open(&pcap_path).await.expect("failed to open file"); let mut pcap_reader = PcapNgReader::new(pcap_file) .await .expect("failed to read PCAP file"); let mut report = Report::new(pcap_path); while let Some(Ok(block)) = pcap_reader.next_block().await { let row = match block { Block::EnhancedPacket(packet) => harness.analyze_pcap_packet(packet), other => { debug!("{pcap_path}: skipping pcap packet {other:?}"); continue; } }; report.process_row(row); } report.print_summary(show_skipped); } async fn analyze_qmdl(qmdl_path: &str, show_skipped: bool) { let mut harness = Harness::new_with_config(&AnalyzerConfig::default()); let qmdl_file = &mut File::open(&qmdl_path).await.expect("failed to open file"); let file_size = qmdl_file .metadata() .await .expect("failed to get QMDL file metadata") .len(); let mut qmdl_reader = QmdlReader::new(qmdl_file, Some(file_size as usize)); let mut qmdl_stream = pin!( qmdl_reader .as_stream() .try_filter(|container| future::ready(container.data_type == DataType::UserSpace)) ); let mut report = Report::new(qmdl_path); while let Some(container) = qmdl_stream .try_next() .await .expect("failed getting QMDL container") { for row in harness.analyze_qmdl_messages(container) { report.process_row(row); } } report.print_summary(show_skipped); } async fn pcapify(qmdl_path: &PathBuf) { let qmdl_file = &mut File::open(&qmdl_path) .await .expect("failed to open qmdl file"); let qmdl_file_size = qmdl_file.metadata().await.unwrap().len(); let mut qmdl_reader = QmdlReader::new(qmdl_file, Some(qmdl_file_size as usize)); let mut pcap_path = qmdl_path.clone(); pcap_path.set_extension("pcapng"); let pcap_file = &mut File::create(&pcap_path) .await .expect("failed to open pcap file"); let mut pcap_writer = GsmtapPcapWriter::new(pcap_file).await.unwrap(); pcap_writer.write_iface_header().await.unwrap(); while let Some(container) = qmdl_reader .get_next_messages_container() .await .expect("failed to get container") { for msg in container.into_messages().into_iter().flatten() { if let Ok(Some((timestamp, parsed))) = gsmtap_parser::parse(msg) { pcap_writer .write_gsmtap_message(parsed, timestamp) .await .expect("failed to write"); } } } info!("wrote pcap to {:?}", &pcap_path); } #[tokio::main] async fn main() { let args = Args::parse(); let level = if args.debug { log::LevelFilter::Debug } else if args.quiet { log::LevelFilter::Warn } else { log::LevelFilter::Info }; simple_logger::SimpleLogger::new() .with_colors(true) .without_timestamps() .with_level(level) //Filter out a stupid massive amount of uneccesary warnings from hampi about undecoded extensions .with_module_level("asn1_codecs", log::LevelFilter::Error) .init() .unwrap(); let harness = Harness::new_with_config(&AnalyzerConfig::default()); info!("Analyzers:"); for analyzer in harness.get_metadata().analyzers { info!( " - {} (v{}): {}", analyzer.name, analyzer.version, analyzer.description ); } for maybe_entry in WalkDir::new(&args.path) { let Ok(entry) = maybe_entry else { error!("failed to open dir entry {maybe_entry:?}"); continue; }; let name = entry.file_name(); let name_str = name.to_str().unwrap(); let path = entry.path(); let path_str = path.to_str().unwrap(); // instead of relying on the QMDL extension, can we check if a file is // QMDL by inspecting the contents? if name_str.ends_with(".qmdl") { info!("**** Beginning analysis of {name_str}"); analyze_qmdl(path_str, args.show_skipped).await; if args.pcapify { pcapify(&path.to_path_buf()).await; } } else if name_str.ends_with(".pcap") || name_str.ends_with(".pcapng") { // TODO: if we've already analyzed a QMDL, skip its corresponding pcap info!("**** Beginning analysis of {name_str}"); analyze_pcap(path_str, args.show_skipped).await; } } }