mirror of
https://github.com/EFForg/rayhunter.git
synced 2026-04-26 23:49:59 -07:00
See https://github.com/EFForg/rayhunter/issues/334 Severity levels low, medium, high are now exposed to the UI in form of dotted, dashed and solid lines. The line on the UI represents the highest-so-far severity seen. Originally this was intended to be represented by Yellow/Orange/Red, but this would mean yet another divergence for colorblind mode. This is colorblind-friendly by default (I think...) As part of this, simplify EventType so that it becomes a flat "level" enum without nested variants. There is also a new debug endpoint that allows one to overwrite the display level directly for testing.
222 lines
7.2 KiB
Rust
222 lines
7.2 KiB
Rust
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<String, u32>,
|
|
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;
|
|
}
|
|
}
|
|
}
|