mirror of
https://github.com/EFForg/rayhunter.git
synced 2026-08-03 04:33:06 -07:00
bef92d4558
This adds a --json flag to rayhunter-check which allows users to save all reports to a JSON file.
244 lines
8.3 KiB
Rust
244 lines
8.3 KiB
Rust
use chrono::{DateTime, FixedOffset};
|
|
use clap::Parser;
|
|
use log::{debug, error, info, warn};
|
|
use pcap_file_tokio::pcapng::{Block, PcapNgReader};
|
|
use rayhunter::{
|
|
analysis::analyzer::{AnalysisRow, AnalyzerConfig, Event, EventType, Harness}, gsmtap::parser as gsmtap_parser, pcap::GsmtapPcapWriter, qmdl::QmdlMessageReader,
|
|
};
|
|
use serde::{Deserialize, Serialize};
|
|
use std::{collections::HashMap, path::PathBuf};
|
|
use tokio::fs::File;
|
|
use walkdir::WalkDir;
|
|
|
|
use crate::json::IncrementalJsonWriter;
|
|
|
|
mod json;
|
|
|
|
#[derive(Parser, Debug)]
|
|
#[command(version, about)]
|
|
struct Args {
|
|
#[arg(short = 'p', long, help = "A file or directory of packet captures")]
|
|
path: PathBuf,
|
|
|
|
#[arg(short = 'P', long, help = "Convert qmdl files to pcap before analysis")]
|
|
pcapify: bool,
|
|
|
|
#[arg(long, help = "Show why some packets were skipped during analysis")]
|
|
show_skipped: bool,
|
|
|
|
#[arg(short = 'j', long, help = "Output report to a JSON file")]
|
|
json: Option<PathBuf>,
|
|
|
|
#[arg(short, long, help = "Only print warnings/errors to stdout")]
|
|
quiet: bool,
|
|
|
|
#[arg(short, long, help = "Show debug messages")]
|
|
debug: bool,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
|
struct EventWithTimestamp {
|
|
timestamp: DateTime<FixedOffset>,
|
|
event: Event,
|
|
}
|
|
|
|
#[derive(Debug, Default, Serialize, Deserialize, PartialEq)]
|
|
struct Report {
|
|
skipped_reasons: HashMap<String, u32>,
|
|
events: Vec<EventWithTimestamp>,
|
|
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;
|
|
}
|
|
}
|
|
self.events.push(EventWithTimestamp { timestamp, event });
|
|
}
|
|
}
|
|
|
|
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, json_writer: Option<&mut IncrementalJsonWriter<File>>) {
|
|
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);
|
|
if let Some(writer) = json_writer {
|
|
writer.write_report(&report).await.expect("failed to write report to JSON");
|
|
}
|
|
}
|
|
|
|
async fn analyze_qmdl(qmdl_path: &str, show_skipped: bool, json_writer: Option<&mut IncrementalJsonWriter<File>>) {
|
|
let mut harness = Harness::new_with_config(&AnalyzerConfig::default());
|
|
let qmdl_file = &mut File::open(&qmdl_path).await.expect("failed to open file");
|
|
let mut qmdl_reader = QmdlMessageReader::new(qmdl_file)
|
|
.await
|
|
.expect("failed to open QmdlReader");
|
|
let mut report = Report::new(qmdl_path);
|
|
while let Some(maybe_message) = qmdl_reader
|
|
.get_next_message()
|
|
.await
|
|
.expect("failed to get message")
|
|
{
|
|
report.process_row(harness.analyze_qmdl_message(maybe_message));
|
|
}
|
|
report.print_summary(show_skipped);
|
|
if let Some(writer) = json_writer {
|
|
writer.write_report(&report).await.expect("failed to write report to JSON");
|
|
}
|
|
}
|
|
|
|
async fn pcapify(qmdl_path: &PathBuf) {
|
|
let qmdl_file = &mut File::open(&qmdl_path)
|
|
.await
|
|
.expect("failed to open qmdl file");
|
|
let mut qmdl_reader = QmdlMessageReader::new(qmdl_file)
|
|
.await
|
|
.expect("failed to open QmdlReader");
|
|
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(maybe_message) = qmdl_reader
|
|
.get_next_message()
|
|
.await
|
|
.expect("failed to get message")
|
|
{
|
|
if let Ok(msg) = maybe_message
|
|
&& let Ok(Some((timestamp, parsed))) = gsmtap_parser::parse(msg)
|
|
{
|
|
pcap_writer
|
|
.write_gsmtap_message(parsed, timestamp, None)
|
|
.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
|
|
};
|
|
rayhunter::init_logging(level);
|
|
|
|
let harness = Harness::new_with_config(&AnalyzerConfig::default());
|
|
let metadata = harness.get_metadata();
|
|
info!("Analyzers:");
|
|
for analyzer in &metadata.analyzers {
|
|
info!(
|
|
" - {} (v{}): {}",
|
|
analyzer.name, analyzer.version, analyzer.description
|
|
);
|
|
}
|
|
|
|
let mut json_writer = None;
|
|
if let Some(json_path) = &args.json {
|
|
let json_file = File::options()
|
|
.write(true)
|
|
.create(true)
|
|
.truncate(true)
|
|
.open(json_path)
|
|
.await
|
|
.expect("failed to create JSON output file");
|
|
let check_path_str = args.path.to_string_lossy();
|
|
json_writer = Some(IncrementalJsonWriter::new(json_file, &check_path_str, &metadata)
|
|
.await
|
|
.expect("failed to create JSON writer"));
|
|
}
|
|
|
|
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();
|
|
if name_str.ends_with(".qmdl") || name_str.ends_with(".qmdl.gz") {
|
|
info!("**** Beginning analysis of {name_str}");
|
|
analyze_qmdl(path_str, args.show_skipped, json_writer.as_mut()).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, json_writer.as_mut()).await;
|
|
}
|
|
}
|
|
|
|
if let Some(writer) = json_writer {
|
|
// if we have a json_writer, we also have args.json
|
|
info!("Writing report to {:?}", args.json.unwrap());
|
|
writer.finish().await.expect("failed to finish writing to JSON file");
|
|
}
|
|
}
|