Files
rayhunter/lib/src/analysis/analyzer.rs
2025-07-16 13:20:14 -07:00

240 lines
7.7 KiB
Rust

use chrono::{DateTime, FixedOffset};
use serde::{Deserialize, Serialize};
use std::borrow::Cow;
use crate::util::RuntimeMetadata;
use crate::{diag::MessagesContainer, gsmtap_parser};
use super::{
connection_redirect_downgrade::ConnectionRedirect2GDowngradeAnalyzer,
imsi_requested::ImsiRequestedAnalyzer, information_element::InformationElement,
null_cipher::NullCipherAnalyzer, priority_2g_downgrade::LteSib6And7DowngradeAnalyzer,
};
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(default)]
pub struct AnalyzerConfig {
pub imsi_requested: bool,
pub connection_redirect_2g_downgrade: bool,
pub lte_sib6_and_7_downgrade: bool,
pub null_cipher: bool,
}
impl Default for AnalyzerConfig {
fn default() -> Self {
AnalyzerConfig {
imsi_requested: true,
connection_redirect_2g_downgrade: true,
lte_sib6_and_7_downgrade: true,
null_cipher: true,
}
}
}
pub const REPORT_VERSION: u32 = 2;
/// Qualitative measure of how severe a Warning event type is.
/// The levels should break down like this:
/// * Low: if combined with a large number of other Warnings, user should investigate
/// * Medium: if combined with a few other Warnings, user should investigate
/// * High: user should investigate
#[derive(Serialize, Debug, Clone)]
pub enum Severity {
Low,
Medium,
High,
}
/// `QualitativeWarning` events will always be shown to the user in some manner,
/// while `Informational` ones may be hidden based on user settings.
#[derive(Serialize, Debug, Clone)]
#[serde(tag = "type")]
pub enum EventType {
Informational,
QualitativeWarning { severity: Severity },
}
/// Events are user-facing signals that can be emitted by an [Analyzer] upon a
/// message being received. They can be used to signifiy an IC detection
/// warning, or just to display some relevant information to the user.
#[derive(Serialize, Debug, Clone)]
pub struct Event {
pub event_type: EventType,
pub message: String,
}
/// An [Analyzer] represents one type of heuristic for detecting an IMSI Catcher
/// (IC). While maintaining some amount of state is useful, be mindful of how
/// much memory your [Analyzer] uses at runtime, since rayhunter may run for
/// many hours at a time with dozens of [Analyzers](Analyzer) working in parallel.
pub trait Analyzer {
/// Returns a user-friendly, concise name for your heuristic.
fn get_name(&self) -> Cow<str>;
/// Returns a user-friendly description of what your heuristic looks for,
/// the types of [Events](Event) it may return, as well as possible false-positive
/// conditions that may trigger an [Event]. If different [Events](Event) have
/// different false-positive conditions, consider including them in its
/// `message` field.
fn get_description(&self) -> Cow<str>;
/// Analyze a single [InformationElement], possibly returning an [Event] if your
/// heuristic deems it relevant. Again, be mindful of any state your
/// [Analyzer] updates per message, since it may be run over hundreds or
/// thousands of them alongside many other [Analyzers](Analyzer).
fn analyze_information_element(&mut self, ie: &InformationElement) -> Option<Event>;
/// Returns a version number for this Analyzer. This should only ever
/// increase in value, and do so whenever substantial changes are made to
/// the Analyzer's heuristic.
fn get_version(&self) -> u32;
}
#[derive(Serialize, Debug)]
pub struct AnalyzerMetadata {
pub name: String,
pub description: String,
pub version: u32,
}
#[derive(Serialize, Debug)]
pub struct ReportMetadata {
pub analyzers: Vec<AnalyzerMetadata>,
pub rayhunter: RuntimeMetadata,
// anytime the format of the report changes, bump this by 1
pub report_version: u32,
}
#[derive(Serialize, Debug)]
pub struct AnalysisRow {
pub packet_timestamp: Option<DateTime<FixedOffset>>,
pub skipped_message_reason: Option<String>,
pub events: Vec<Option<Event>>,
}
impl AnalysisRow {
pub fn is_empty(&self) -> bool {
self.skipped_message_reason.is_none() && !self.contains_warnings()
}
pub fn contains_warnings(&self) -> bool {
for event in self.events.iter().flatten() {
if matches!(event.event_type, EventType::QualitativeWarning { .. }) {
return true;
}
}
false
}
}
pub struct Harness {
analyzers: Vec<Box<dyn Analyzer + Send>>,
}
impl Default for Harness {
fn default() -> Self {
Self::new()
}
}
impl Harness {
pub fn new() -> Self {
Self {
analyzers: Vec::new(),
}
}
pub fn new_with_config(analyzer_config: &AnalyzerConfig) -> Self {
let mut harness = Harness::new();
if analyzer_config.imsi_requested {
harness.add_analyzer(Box::new(ImsiRequestedAnalyzer::new()));
}
if analyzer_config.connection_redirect_2g_downgrade {
harness.add_analyzer(Box::new(ConnectionRedirect2GDowngradeAnalyzer {}));
}
if analyzer_config.lte_sib6_and_7_downgrade {
harness.add_analyzer(Box::new(LteSib6And7DowngradeAnalyzer {}));
}
if analyzer_config.null_cipher {
harness.add_analyzer(Box::new(NullCipherAnalyzer {}));
}
harness
}
pub fn add_analyzer(&mut self, analyzer: Box<dyn Analyzer + Send>) {
self.analyzers.push(analyzer);
}
pub fn analyze_qmdl_messages(&mut self, container: MessagesContainer) -> Vec<AnalysisRow> {
let mut rows = Vec::new();
for maybe_qmdl_message in container.into_messages() {
rows.push(AnalysisRow {
packet_timestamp: None,
skipped_message_reason: None,
events: Vec::new(),
});
// unwrap is safe here since we just pushed a value
let row = rows.last_mut().unwrap();
let qmdl_message = match maybe_qmdl_message {
Ok(msg) => msg,
Err(err) => {
row.skipped_message_reason = Some(format!("{err:?}"));
continue;
}
};
let gsmtap_message = match gsmtap_parser::parse(qmdl_message) {
Ok(msg) => msg,
Err(err) => {
row.skipped_message_reason = Some(format!("{err:?}"));
continue;
}
};
let Some((timestamp, gsmtap_msg)) = gsmtap_message else {
continue;
};
row.packet_timestamp = Some(timestamp.to_datetime());
let element = match InformationElement::try_from(&gsmtap_msg) {
Ok(element) => element,
Err(err) => {
row.skipped_message_reason = Some(format!("{err:?}"));
continue;
}
};
row.events = self.analyze_information_element(&element);
}
rows
}
fn analyze_information_element(&mut self, ie: &InformationElement) -> Vec<Option<Event>> {
self.analyzers
.iter_mut()
.map(|analyzer| analyzer.analyze_information_element(ie))
.collect()
}
pub fn get_metadata(&self) -> ReportMetadata {
let mut analyzers = Vec::new();
for analyzer in &self.analyzers {
analyzers.push(AnalyzerMetadata {
name: analyzer.get_name().to_string(),
description: analyzer.get_description().to_string(),
version: analyzer.get_version(),
});
}
let rayhunter = RuntimeMetadata::new();
ReportMetadata {
analyzers,
rayhunter,
report_version: REPORT_VERSION,
}
}
}