mirror of
https://github.com/EFForg/rayhunter.git
synced 2026-06-19 02:49:43 -07:00
Expose severity to display
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.
This commit is contained in:
committed by
Cooper Quintin
parent
6927da49b4
commit
781d11ed72
Generated
+1
@@ -2721,6 +2721,7 @@ dependencies = [
|
||||
"pcap-file-tokio",
|
||||
"pycrate-rs",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"telcom-parser",
|
||||
"thiserror 1.0.69",
|
||||
"tokio",
|
||||
|
||||
+2
-2
@@ -65,10 +65,10 @@ impl Report {
|
||||
EventType::Informational => {
|
||||
info!("{}: INFO - {} {}", self.file_path, timestamp, event.message,);
|
||||
}
|
||||
EventType::QualitativeWarning { severity } => {
|
||||
EventType::Low | EventType::Medium | EventType::High => {
|
||||
warn!(
|
||||
"{}: WARNING (Severity: {:?}) - {} {}",
|
||||
self.file_path, severity, timestamp, event.message,
|
||||
self.file_path, event.event_type, timestamp, event.message,
|
||||
);
|
||||
self.warnings += 1;
|
||||
}
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ tokio-util = { version = "0.7.10", features = ["rt", "io", "compat"] }
|
||||
futures-macro = "0.3.30"
|
||||
include_dir = "0.7.3"
|
||||
chrono = { version = "0.4.31", features = ["serde"] }
|
||||
tokio-stream = { version = "0.1.14", default-features = false }
|
||||
tokio-stream = { version = "0.1.14", default-features = false, features = ["io-util"] }
|
||||
futures = { version = "0.3.30", default-features = false }
|
||||
serde_json = "1.0.114"
|
||||
image = { version = "0.25.1", default-features = false, features = ["png", "gif"] }
|
||||
|
||||
+10
-6
@@ -1,5 +1,5 @@
|
||||
use std::sync::Arc;
|
||||
use std::{future, pin};
|
||||
use std::{cmp, future, pin};
|
||||
|
||||
use axum::Json;
|
||||
use axum::{
|
||||
@@ -8,7 +8,7 @@ use axum::{
|
||||
};
|
||||
use futures::TryStreamExt;
|
||||
use log::{error, info};
|
||||
use rayhunter::analysis::analyzer::{AnalyzerConfig, Harness};
|
||||
use rayhunter::analysis::analyzer::{AnalyzerConfig, EventType, Harness};
|
||||
use rayhunter::diag::{DataType, MessagesContainer};
|
||||
use rayhunter::qmdl::QmdlReader;
|
||||
use serde::Serialize;
|
||||
@@ -47,15 +47,19 @@ impl AnalysisWriter {
|
||||
|
||||
// Runs the analysis harness on the given container, serializing the results
|
||||
// to the analysis file, returning the whether any warnings were detected
|
||||
pub async fn analyze(&mut self, container: MessagesContainer) -> Result<bool, std::io::Error> {
|
||||
let mut warning_detected = false;
|
||||
pub async fn analyze(
|
||||
&mut self,
|
||||
container: MessagesContainer,
|
||||
) -> Result<EventType, std::io::Error> {
|
||||
let mut max_type = EventType::Informational;
|
||||
|
||||
for row in self.harness.analyze_qmdl_messages(container) {
|
||||
if !row.is_empty() {
|
||||
self.write(&row).await?;
|
||||
}
|
||||
warning_detected |= row.contains_warnings();
|
||||
max_type = cmp::max(max_type, row.get_max_event_type());
|
||||
}
|
||||
Ok(warning_detected)
|
||||
Ok(max_type)
|
||||
}
|
||||
|
||||
async fn write<T: Serialize>(&mut self, value: &T) -> Result<(), std::io::Error> {
|
||||
|
||||
+37
-15
@@ -8,17 +8,19 @@ use axum::extract::{Path, State};
|
||||
use axum::http::StatusCode;
|
||||
use axum::http::header::CONTENT_TYPE;
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use futures::{StreamExt, TryStreamExt};
|
||||
use futures::{StreamExt, TryStreamExt, future};
|
||||
use log::{debug, error, info, warn};
|
||||
use rayhunter::analysis::analyzer::AnalyzerConfig;
|
||||
use tokio::fs::File;
|
||||
use tokio::io::{AsyncBufReadExt, BufReader};
|
||||
use tokio::sync::mpsc::{Receiver, Sender};
|
||||
use tokio::sync::{RwLock, oneshot};
|
||||
use tokio_stream::wrappers::LinesStream;
|
||||
use tokio_util::task::TaskTracker;
|
||||
|
||||
use rayhunter::analysis::analyzer::{AnalysisLineNormalizer, AnalyzerConfig, EventType};
|
||||
use rayhunter::diag::{DataType, MessagesContainer};
|
||||
use rayhunter::diag_device::DiagDevice;
|
||||
use rayhunter::qmdl::QmdlWriter;
|
||||
use tokio::fs::File;
|
||||
use tokio::sync::mpsc::{Receiver, Sender};
|
||||
use tokio::sync::{RwLock, oneshot};
|
||||
use tokio_util::io::ReaderStream;
|
||||
use tokio_util::task::TaskTracker;
|
||||
|
||||
use crate::analysis::{AnalysisCtrlMessage, AnalysisWriter};
|
||||
use crate::display;
|
||||
@@ -45,6 +47,7 @@ pub struct DiagTask {
|
||||
analyzer_config: AnalyzerConfig,
|
||||
notification_channel: tokio::sync::mpsc::Sender<Notification>,
|
||||
state: DiagState,
|
||||
max_type_seen: EventType,
|
||||
}
|
||||
|
||||
enum DiagState {
|
||||
@@ -68,6 +71,7 @@ impl DiagTask {
|
||||
analyzer_config,
|
||||
notification_channel,
|
||||
state: DiagState::Stopped,
|
||||
max_type_seen: EventType::Informational,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,16 +198,13 @@ impl DiagTask {
|
||||
.await
|
||||
.expect("failed to update qmdl file size");
|
||||
debug!("done!");
|
||||
let heuristic_warning = analysis_writer
|
||||
let max_type = analysis_writer
|
||||
.analyze(container)
|
||||
.await
|
||||
.expect("failed to analyze container");
|
||||
if heuristic_warning {
|
||||
|
||||
if max_type > EventType::Informational {
|
||||
info!("a heuristic triggered on this run!");
|
||||
self.ui_update_sender
|
||||
.send(display::DisplayState::WarningDetected)
|
||||
.await
|
||||
.expect("couldn't send ui update message: {}");
|
||||
self.notification_channel
|
||||
.send(Notification::new(
|
||||
"heuristic-warning".to_string(),
|
||||
@@ -213,6 +214,19 @@ impl DiagTask {
|
||||
.await
|
||||
.expect("Failed to send to notification channel");
|
||||
}
|
||||
|
||||
if max_type > self.max_type_seen {
|
||||
self.max_type_seen = max_type;
|
||||
if self.max_type_seen > EventType::Informational {
|
||||
self.ui_update_sender
|
||||
.send(display::DisplayState::WarningDetected {
|
||||
event_type: self.max_type_seen,
|
||||
})
|
||||
.await
|
||||
.expect("couldn't send ui update message: {}");
|
||||
}
|
||||
|
||||
}
|
||||
} else {
|
||||
debug!("no qmdl_writer set, continuing...");
|
||||
}
|
||||
@@ -422,9 +436,17 @@ pub async fn get_analysis_report(
|
||||
.open_entry_analysis(entry_index)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e:?}")))?;
|
||||
let analysis_stream = ReaderStream::new(analysis_file);
|
||||
|
||||
// Read and normalize the NDJSON file
|
||||
let reader = BufReader::new(analysis_file);
|
||||
let lines_stream = LinesStream::new(reader.lines());
|
||||
|
||||
let mut normalizer = AnalysisLineNormalizer::new();
|
||||
let normalized_stream = lines_stream
|
||||
.try_filter(|line| future::ready(!line.is_empty()))
|
||||
.map_ok(move |line| normalizer.normalize_line(line));
|
||||
|
||||
let headers = [(CONTENT_TYPE, "application/x-ndjson")];
|
||||
let body = Body::from_stream(analysis_stream);
|
||||
let body = Body::from_stream(normalized_stream);
|
||||
Ok((headers, body).into_response())
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ use std::time::Duration;
|
||||
|
||||
use crate::config;
|
||||
use crate::display::DisplayState;
|
||||
use rayhunter::analysis::analyzer::EventType;
|
||||
|
||||
use log::{error, info};
|
||||
use tokio::sync::mpsc::Receiver;
|
||||
@@ -20,6 +21,13 @@ pub struct Dimensions {
|
||||
pub width: u32,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum LinePattern {
|
||||
Solid,
|
||||
Dashed, // _ _ _ _
|
||||
Dotted, // . . . .
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum Color {
|
||||
@@ -48,18 +56,36 @@ impl Color {
|
||||
}
|
||||
}
|
||||
|
||||
impl Color {
|
||||
fn from_state(state: DisplayState, colorblind_mode: bool) -> Self {
|
||||
match state {
|
||||
DisplayState::Paused => Color::White,
|
||||
DisplayState::Recording => {
|
||||
if colorblind_mode {
|
||||
Color::Blue
|
||||
} else {
|
||||
Color::Green
|
||||
}
|
||||
fn display_style_from_state(state: DisplayState, colorblind_mode: bool) -> (Color, LinePattern) {
|
||||
match state {
|
||||
DisplayState::Paused => (Color::White, LinePattern::Solid),
|
||||
DisplayState::Recording => {
|
||||
if colorblind_mode {
|
||||
(Color::Blue, LinePattern::Solid)
|
||||
} else {
|
||||
(Color::Green, LinePattern::Solid)
|
||||
}
|
||||
DisplayState::WarningDetected => Color::Red,
|
||||
}
|
||||
DisplayState::WarningDetected { event_type } => {
|
||||
let pattern = match event_type {
|
||||
EventType::Informational => LinePattern::Solid,
|
||||
EventType::Low => LinePattern::Dotted,
|
||||
EventType::Medium => LinePattern::Dashed,
|
||||
EventType::High => LinePattern::Solid,
|
||||
};
|
||||
|
||||
let color = match event_type {
|
||||
EventType::Informational => {
|
||||
if colorblind_mode {
|
||||
Color::Blue
|
||||
} else {
|
||||
Color::Green
|
||||
}
|
||||
}
|
||||
_ => Color::Red,
|
||||
};
|
||||
|
||||
(color, pattern)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -120,11 +146,28 @@ pub trait GenericFramebuffer: Send + 'static {
|
||||
}
|
||||
|
||||
async fn draw_line(&mut self, color: Color, height: u32) {
|
||||
self.draw_patterned_line(color, height, LinePattern::Solid)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn draw_patterned_line(&mut self, color: Color, height: u32, pattern: LinePattern) {
|
||||
let width = self.dimensions().width;
|
||||
let px_num = height * width;
|
||||
let mut buffer = Vec::new();
|
||||
for _ in 0..px_num {
|
||||
buffer.push(color.rgb());
|
||||
|
||||
for _row in 0..height {
|
||||
for col in 0..width {
|
||||
let should_draw = match pattern {
|
||||
LinePattern::Solid => true,
|
||||
LinePattern::Dashed => (col / 4) % 2 == 0, // 4 pixels on, 4 pixels off
|
||||
LinePattern::Dotted => col % 4 == 0, // 1 pixel on, 3 pixels off
|
||||
};
|
||||
|
||||
if should_draw {
|
||||
buffer.push(color.rgb());
|
||||
} else {
|
||||
buffer.push((0, 0, 0)); // Black background
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.write_buffer(buffer).await
|
||||
@@ -145,7 +188,7 @@ pub fn update_ui(
|
||||
}
|
||||
|
||||
let colorblind_mode = config.colorblind_mode;
|
||||
let mut display_color = Color::from_state(DisplayState::Recording, colorblind_mode);
|
||||
let mut display_style = display_style_from_state(DisplayState::Recording, colorblind_mode);
|
||||
|
||||
task_tracker.spawn(async move {
|
||||
// this feels wrong, is there a more rusty way to do this?
|
||||
@@ -176,7 +219,7 @@ pub fn update_ui(
|
||||
}
|
||||
match ui_update_rx.try_recv() {
|
||||
Ok(state) => {
|
||||
display_color = Color::from_state(state, colorblind_mode);
|
||||
display_style = display_style_from_state(state, colorblind_mode);
|
||||
}
|
||||
Err(tokio::sync::mpsc::error::TryRecvError::Empty) => {}
|
||||
Err(e) => error!("error receiving framebuffer update message: {e}"),
|
||||
@@ -196,7 +239,8 @@ pub fn update_ui(
|
||||
// unknown value is used
|
||||
_ => {}
|
||||
};
|
||||
fb.draw_line(display_color, 2).await;
|
||||
let (color, pattern) = display_style;
|
||||
fb.draw_patterned_line(color, 2, pattern).await;
|
||||
tokio::time::sleep(Duration::from_millis(1000)).await;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
use rayhunter::analysis::analyzer::EventType;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
mod generic_framebuffer;
|
||||
|
||||
pub mod headless;
|
||||
@@ -9,9 +12,15 @@ pub mod tplink_onebit;
|
||||
pub mod uz801;
|
||||
pub mod wingtech;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
#[derive(Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||
pub enum DisplayState {
|
||||
/// We're recording but no warning has been found yet.
|
||||
Recording,
|
||||
/// We're not recording.
|
||||
Paused,
|
||||
WarningDetected,
|
||||
/// A non-informational event has been detected.
|
||||
///
|
||||
/// Note that EventType::Informational is never sent through this. If it is, it's the same as
|
||||
/// Recording
|
||||
WarningDetected { event_type: EventType },
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/// Display module for Tmobile TMOHS1, blink LEDs on the front of the device.
|
||||
/// DisplayState::Recording => Signal LED slowly blinks blue.
|
||||
/// DisplayState::Paused => WiFi LED blinks white.
|
||||
/// DisplayState::WarningDetected => Signal LED slowly blinks red.
|
||||
/// DisplayState::WarningDetected { .. } => Signal LED slowly blinks red.
|
||||
use log::{error, info};
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::sync::oneshot;
|
||||
@@ -68,7 +68,7 @@ pub fn update_ui(
|
||||
stop_blinking(led!("signal_red")).await;
|
||||
start_blinking(led!("signal_blue")).await;
|
||||
}
|
||||
DisplayState::WarningDetected => {
|
||||
DisplayState::WarningDetected { .. } => {
|
||||
stop_blinking(led!("wlan_white")).await;
|
||||
stop_blinking(led!("signal_blue")).await;
|
||||
start_blinking(led!("signal_red")).await;
|
||||
|
||||
@@ -136,7 +136,7 @@ pub fn update_ui(
|
||||
match ui_update_rx.try_recv() {
|
||||
Ok(DisplayState::Paused) => pixels = STATUS_PAUSED,
|
||||
Ok(DisplayState::Recording) => pixels = STATUS_SMILING,
|
||||
Ok(DisplayState::WarningDetected) => pixels = STATUS_WARNING,
|
||||
Ok(DisplayState::WarningDetected { .. }) => pixels = STATUS_WARNING,
|
||||
Err(tokio::sync::mpsc::error::TryRecvError::Empty) => {}
|
||||
Err(e) => {
|
||||
error!("error receiving framebuffer update message: {e}");
|
||||
|
||||
@@ -73,7 +73,7 @@ pub fn update_ui(
|
||||
led_off(led!("wifi")).await;
|
||||
led_on(led!("green")).await;
|
||||
}
|
||||
DisplayState::WarningDetected => {
|
||||
DisplayState::WarningDetected { .. } => {
|
||||
led_off(led!("green")).await;
|
||||
led_off(led!("wifi")).await;
|
||||
led_on(led!("red")).await;
|
||||
|
||||
+5
-1
@@ -20,7 +20,9 @@ use crate::error::RayhunterError;
|
||||
use crate::notifications::{NotificationService, run_notification_worker};
|
||||
use crate::pcap::get_pcap;
|
||||
use crate::qmdl_store::RecordingStore;
|
||||
use crate::server::{ServerState, get_config, get_qmdl, get_zip, serve_static, set_config};
|
||||
use crate::server::{
|
||||
ServerState, debug_set_display_state, get_config, get_qmdl, get_zip, serve_static, set_config,
|
||||
};
|
||||
use crate::stats::{get_qmdl_manifest, get_system_stats};
|
||||
|
||||
use analysis::{
|
||||
@@ -62,6 +64,7 @@ fn get_router() -> AppRouter {
|
||||
.route("/api/analysis/{name}", post(start_analysis))
|
||||
.route("/api/config", get(get_config))
|
||||
.route("/api/config", post(set_config))
|
||||
.route("/api/debug/display-state", post(debug_set_display_state))
|
||||
.route("/", get(|| async { Redirect::permanent("/index.html") }))
|
||||
.route("/{*path}", get(serve_static))
|
||||
}
|
||||
@@ -300,6 +303,7 @@ async fn run_with_config(
|
||||
analysis_status_lock,
|
||||
analysis_sender: analysis_tx,
|
||||
daemon_restart_tx: Arc::new(RwLock::new(Some(daemon_restart_tx))),
|
||||
ui_update_sender: Some(ui_update_tx),
|
||||
});
|
||||
run_server(&task_tracker, state, server_shutdown_rx).await;
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ use tokio_util::io::ReaderStream;
|
||||
use crate::DiagDeviceCtrlMessage;
|
||||
use crate::analysis::{AnalysisCtrlMessage, AnalysisStatus};
|
||||
use crate::config::Config;
|
||||
use crate::display::DisplayState;
|
||||
use crate::pcap::generate_pcap_data;
|
||||
use crate::qmdl_store::RecordingStore;
|
||||
|
||||
@@ -32,6 +33,7 @@ pub struct ServerState {
|
||||
pub analysis_status_lock: Arc<RwLock<AnalysisStatus>>,
|
||||
pub analysis_sender: Sender<AnalysisCtrlMessage>,
|
||||
pub daemon_restart_tx: Arc<RwLock<Option<oneshot::Sender<()>>>>,
|
||||
pub ui_update_sender: Option<Sender<DisplayState>>,
|
||||
}
|
||||
|
||||
pub async fn get_qmdl(
|
||||
@@ -242,6 +244,29 @@ pub async fn get_zip(
|
||||
Ok((headers, body).into_response())
|
||||
}
|
||||
|
||||
pub async fn debug_set_display_state(
|
||||
State(state): State<Arc<ServerState>>,
|
||||
Json(display_state): Json<DisplayState>,
|
||||
) -> Result<(StatusCode, String), (StatusCode, String)> {
|
||||
if let Some(ui_sender) = &state.ui_update_sender {
|
||||
ui_sender.send(display_state).await.map_err(|_| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"failed to send display state update".to_string(),
|
||||
)
|
||||
})?;
|
||||
Ok((
|
||||
StatusCode::OK,
|
||||
"display state updated successfully".to_string(),
|
||||
))
|
||||
} else {
|
||||
Err((
|
||||
StatusCode::SERVICE_UNAVAILABLE,
|
||||
"display system not available".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -307,6 +332,7 @@ mod tests {
|
||||
analysis_status_lock: Arc::new(RwLock::new(analysis_status)),
|
||||
analysis_sender: analysis_tx,
|
||||
daemon_restart_tx: Arc::new(RwLock::new(None)),
|
||||
ui_update_sender: None,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,43 +1,7 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { AnalysisRowType, EventType, parse_finished_report, Severity } from './analysis.svelte';
|
||||
import { AnalysisRowType, parse_finished_report } from './analysis.svelte';
|
||||
import { type NewlineDeliminatedJson } from './ndjson';
|
||||
|
||||
const SAMPLE_V1_REPORT_NDJSON: NewlineDeliminatedJson = [
|
||||
{
|
||||
analyzers: [
|
||||
{
|
||||
name: 'Analyzer 1',
|
||||
description: 'A first analyzer',
|
||||
},
|
||||
{
|
||||
name: 'Analyzer 2',
|
||||
description: 'A second analyzer',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
timestamp: '2024-10-08T13:25:43.011689003-07:00',
|
||||
skipped_message_reasons: ['The reason why the message was skipped'],
|
||||
analysis: [],
|
||||
},
|
||||
{
|
||||
timestamp: '2024-10-08T13:25:43.480872496-07:00',
|
||||
skipped_message_reasons: [],
|
||||
analysis: [
|
||||
{
|
||||
timestamp: '2024-08-19T03:33:54.318Z',
|
||||
events: [
|
||||
null,
|
||||
{
|
||||
event_type: { type: 'QualitativeWarning', severity: 'Low' },
|
||||
message: 'Something nasty happened',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const SAMPLE_V2_REPORT_NDJSON: NewlineDeliminatedJson = [
|
||||
{
|
||||
analyzers: [
|
||||
@@ -62,7 +26,7 @@ const SAMPLE_V2_REPORT_NDJSON: NewlineDeliminatedJson = [
|
||||
events: [
|
||||
null,
|
||||
{
|
||||
event_type: { type: 'QualitativeWarning', severity: 'Low' },
|
||||
event_type: 'Low',
|
||||
message: 'Something nasty happened',
|
||||
},
|
||||
],
|
||||
@@ -70,40 +34,6 @@ const SAMPLE_V2_REPORT_NDJSON: NewlineDeliminatedJson = [
|
||||
];
|
||||
|
||||
describe('analysis report parsing', () => {
|
||||
it('parses v1 example analysis', () => {
|
||||
const report = parse_finished_report(SAMPLE_V1_REPORT_NDJSON);
|
||||
expect(report.metadata.report_version).toEqual(1);
|
||||
expect(report.metadata.analyzers).toEqual([
|
||||
{
|
||||
name: 'Analyzer 1',
|
||||
description: 'A first analyzer',
|
||||
version: 0,
|
||||
},
|
||||
{
|
||||
name: 'Analyzer 2',
|
||||
description: 'A second analyzer',
|
||||
version: 0,
|
||||
},
|
||||
]);
|
||||
expect(report.rows).toHaveLength(2);
|
||||
expect(report.rows[0].type).toBe(AnalysisRowType.Skipped);
|
||||
if (report.rows[1].type === AnalysisRowType.Analysis) {
|
||||
const row = report.rows[1];
|
||||
expect(row.events).toHaveLength(2);
|
||||
expect(row.events[0]).toBeNull();
|
||||
const event = row.events[1];
|
||||
const expected_timestamp = new Date('2024-08-19T03:33:54.318Z');
|
||||
expect(row.packet_timestamp.getTime()).toEqual(expected_timestamp.getTime());
|
||||
if (event !== null && event.type === EventType.Warning) {
|
||||
expect(event.severity).toEqual(Severity.Low);
|
||||
} else {
|
||||
throw 'wrong event type';
|
||||
}
|
||||
} else {
|
||||
throw 'wrong row type';
|
||||
}
|
||||
});
|
||||
|
||||
it('parses v2 example analysis', () => {
|
||||
const report = parse_finished_report(SAMPLE_V2_REPORT_NDJSON);
|
||||
expect(report.metadata.report_version).toEqual(2);
|
||||
@@ -128,11 +58,7 @@ describe('analysis report parsing', () => {
|
||||
const event = row.events[1];
|
||||
const expected_timestamp = new Date('2024-08-19T03:33:54.318Z');
|
||||
expect(row.packet_timestamp.getTime()).toEqual(expected_timestamp.getTime());
|
||||
if (event !== null && event.type === EventType.Warning) {
|
||||
expect(event.severity).toEqual(Severity.Low);
|
||||
} else {
|
||||
throw 'wrong event type';
|
||||
}
|
||||
expect(event!.event_type).toEqual('Low');
|
||||
} else {
|
||||
throw 'wrong row type';
|
||||
}
|
||||
|
||||
@@ -21,17 +21,7 @@ export class ReportMetadata {
|
||||
constructor(ndjson: any) {
|
||||
this.analyzers = ndjson.analyzers;
|
||||
this.rayhunter = ndjson.rayhunter;
|
||||
if (ndjson.report_version === undefined) {
|
||||
this.report_version = 1;
|
||||
// we consider our legacy (unversioned) heuristics to be v0 --
|
||||
// this'll let us clearly differentiate some known false-positive
|
||||
// results from the pre-versioned era from v1 heuristics
|
||||
this.analyzers.forEach((analyzer) => {
|
||||
analyzer.version = 0;
|
||||
});
|
||||
} else {
|
||||
this.report_version = ndjson.report_version;
|
||||
}
|
||||
this.report_version = ndjson.report_version || 2; // Default to v2
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,77 +54,22 @@ export type PacketAnalysis = {
|
||||
events: Event[];
|
||||
};
|
||||
|
||||
export type Event = QualitativeWarning | InformationalEvent | null;
|
||||
export enum EventType {
|
||||
Informational,
|
||||
Warning,
|
||||
}
|
||||
export type EventType = 'Informational' | 'Low' | 'Medium' | 'High';
|
||||
|
||||
export type QualitativeWarning = {
|
||||
type: EventType.Warning;
|
||||
severity: Severity;
|
||||
export type Event = {
|
||||
event_type: EventType;
|
||||
message: string;
|
||||
};
|
||||
|
||||
export enum Severity {
|
||||
Low,
|
||||
Medium,
|
||||
High,
|
||||
}
|
||||
|
||||
export type InformationalEvent = {
|
||||
type: EventType.Informational;
|
||||
message: string;
|
||||
};
|
||||
} | null;
|
||||
|
||||
function get_event(event_json: any): Event {
|
||||
if (event_json.event_type.type === 'Informational') {
|
||||
return {
|
||||
type: EventType.Informational,
|
||||
message: event_json.message,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
type: EventType.Warning,
|
||||
severity:
|
||||
event_json.event_type.severity === 'High'
|
||||
? Severity.High
|
||||
: event_json.event_type.severity === 'Medium'
|
||||
? Severity.Medium
|
||||
: Severity.Low,
|
||||
message: event_json.message,
|
||||
};
|
||||
if (!['Informational', 'Low', 'Medium', 'High'].includes(event_json.event_type)) {
|
||||
throw `Invalid/unhandled event type: ${event_json.event_type}`;
|
||||
}
|
||||
|
||||
return event_json;
|
||||
}
|
||||
|
||||
function get_v1_rows(row_jsons: any[]): AnalysisRow[] {
|
||||
const rows: AnalysisRow[] = [];
|
||||
for (const row_json of row_jsons) {
|
||||
for (const reason of row_json.skipped_message_reasons) {
|
||||
rows.push({
|
||||
type: AnalysisRowType.Skipped,
|
||||
reason,
|
||||
});
|
||||
}
|
||||
for (const analysis_json of row_json.analysis) {
|
||||
const events: Event[] = analysis_json.events.map((event_json: any): Event | null => {
|
||||
if (event_json === null) {
|
||||
return null;
|
||||
} else {
|
||||
return get_event(event_json);
|
||||
}
|
||||
});
|
||||
rows.push({
|
||||
type: AnalysisRowType.Analysis,
|
||||
packet_timestamp: new Date(analysis_json.timestamp),
|
||||
events,
|
||||
});
|
||||
}
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
function get_v2_rows(row_jsons: any[]): AnalysisRow[] {
|
||||
function get_rows(row_jsons: any[]): AnalysisRow[] {
|
||||
const rows: AnalysisRow[] = [];
|
||||
for (const row_json of row_jsons) {
|
||||
if (row_json.skipped_message_reason) {
|
||||
@@ -170,7 +105,7 @@ function get_report_stats(rows: AnalysisRow[]): ReportStatistics {
|
||||
} else {
|
||||
for (const event of row.events) {
|
||||
if (event !== null) {
|
||||
if (event.type === EventType.Informational) {
|
||||
if (event.event_type === 'Informational') {
|
||||
num_informational_logs++;
|
||||
} else {
|
||||
num_warnings++;
|
||||
@@ -188,12 +123,7 @@ function get_report_stats(rows: AnalysisRow[]): ReportStatistics {
|
||||
|
||||
export function parse_finished_report(report_json: NewlineDeliminatedJson): AnalysisReport {
|
||||
const metadata = new ReportMetadata(report_json[0]);
|
||||
let rows;
|
||||
if (metadata.report_version === 1) {
|
||||
rows = get_v1_rows(report_json.slice(1));
|
||||
} else {
|
||||
rows = get_v2_rows(report_json.slice(1));
|
||||
}
|
||||
const rows = get_rows(report_json.slice(1));
|
||||
const statistics = get_report_stats(rows);
|
||||
return {
|
||||
statistics,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { AnalysisRowType, EventType, type AnalysisReport } from '$lib/analysis.svelte';
|
||||
import { AnalysisRowType, type AnalysisReport } from '$lib/analysis.svelte';
|
||||
let {
|
||||
report,
|
||||
}: {
|
||||
@@ -50,30 +50,19 @@
|
||||
{#each row.events as event, analyzerIndex}
|
||||
{#if event !== null}
|
||||
{@const analyzer = analyzers[analyzerIndex]}
|
||||
{@const event_type_class = {
|
||||
Informational: '',
|
||||
Low: 'bg-red-200',
|
||||
Medium: 'bg-red-400',
|
||||
High: 'bg-red-600',
|
||||
}[event.event_type]}
|
||||
<tr class="even:bg-gray-200 odd:bg-white">
|
||||
{#if event.type === EventType.Warning}
|
||||
{@const severity = ['Low', 'Medium', 'High'][
|
||||
event.severity
|
||||
]}
|
||||
{@const severity_class = [
|
||||
'bg-red-200',
|
||||
'bg-red-400',
|
||||
'bg-red-600',
|
||||
][event.severity]}
|
||||
<td class="p-2">{date_formatter.format(parsed_date)}</td
|
||||
>
|
||||
<td class="p-2">{analyzer.name} v{analyzer.version}</td>
|
||||
<td class="p-2">{event.message}</td>
|
||||
<td class="p-2 {severity_class} text-center"
|
||||
>{severity}</td
|
||||
>
|
||||
{:else if event.type === EventType.Informational}
|
||||
<td class="p-2">{date_formatter.format(parsed_date)}</td
|
||||
>
|
||||
<td class="p-2">{analyzer.name} v{analyzer.version}</td>
|
||||
<td class="p-2">{event.message}</td>
|
||||
<td class="p-2">Info</td>
|
||||
{/if}
|
||||
<td class="p-2">{date_formatter.format(parsed_date)}</td>
|
||||
<td class="p-2">{analyzer.name} v{analyzer.version}</td>
|
||||
<td class="p-2">{event.message}</td>
|
||||
<td class="p-2 {event_type_class} text-center"
|
||||
>{event.event_type}</td
|
||||
>
|
||||
</tr>
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
+4
-1
@@ -21,7 +21,10 @@ pcap-file-tokio = "0.1.0"
|
||||
pycrate-rs = { git = "https://github.com/EFForg/pycrate-rs" }
|
||||
thiserror = "1.0.50"
|
||||
telcom-parser = { path = "../telcom-parser" }
|
||||
tokio = { version = "1.44.2", default-features = false, features = ["time", "rt", "macros"] }
|
||||
tokio = { version = "1.44.2", default-features = false, features = ["time", "rt", "macros", "fs"] }
|
||||
futures = { version = "0.3.30", default-features = false }
|
||||
serde = { version = "1.0.197", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
num_enum = "0.7.4"
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
+236
-25
@@ -43,31 +43,63 @@ impl Default for AnalyzerConfig {
|
||||
|
||||
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,
|
||||
/// The severity level of an event.
|
||||
///
|
||||
/// Informational does not result in any alert on the display.
|
||||
#[derive(Serialize, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum EventType {
|
||||
Informational = 0,
|
||||
Low = 1,
|
||||
Medium = 2,
|
||||
High = 3,
|
||||
}
|
||||
|
||||
/// `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 },
|
||||
impl<'de> Deserialize<'de> for EventType {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
use serde::de::Error;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(tag = "type")]
|
||||
enum OldEventType {
|
||||
QualitativeWarning { severity: String },
|
||||
Informational,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum EventTypeHelper {
|
||||
New(String),
|
||||
Old(OldEventType),
|
||||
}
|
||||
|
||||
match EventTypeHelper::deserialize(deserializer)? {
|
||||
EventTypeHelper::New(s) => match s.as_str() {
|
||||
"Informational" => Ok(EventType::Informational),
|
||||
"Low" => Ok(EventType::Low),
|
||||
"Medium" => Ok(EventType::Medium),
|
||||
"High" => Ok(EventType::High),
|
||||
_ => Err(D::Error::custom(format!("unknown EventType: {s}"))),
|
||||
},
|
||||
EventTypeHelper::Old(old) => match old {
|
||||
OldEventType::Informational => Ok(EventType::Informational),
|
||||
OldEventType::QualitativeWarning { severity } => match severity.as_str() {
|
||||
"Low" => Ok(EventType::Low),
|
||||
"Medium" => Ok(EventType::Medium),
|
||||
"High" => Ok(EventType::High),
|
||||
_ => Err(D::Error::custom(format!("unknown 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)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct Event {
|
||||
pub event_type: EventType,
|
||||
pub message: String,
|
||||
@@ -100,21 +132,77 @@ pub trait Analyzer {
|
||||
fn get_version(&self) -> u32;
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct AnalyzerMetadata {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub version: u32,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(default)]
|
||||
#[derive(Default)]
|
||||
pub struct ReportMetadata {
|
||||
pub analyzers: Vec<AnalyzerMetadata>,
|
||||
pub rayhunter: RuntimeMetadata,
|
||||
|
||||
// anytime the format of the report changes, bump this by 1
|
||||
//
|
||||
// the default is 0. we consider our legacy (unversioned) heuristics to be v0 -- this'll let us
|
||||
// clearly differentiate some known false-positive-results from the pre-versioned era from v1
|
||||
// heuristics
|
||||
pub report_version: u32,
|
||||
}
|
||||
|
||||
impl ReportMetadata {
|
||||
/// Normalize the report metadata to the current version
|
||||
pub fn normalize(&mut self) {
|
||||
self.report_version = REPORT_VERSION;
|
||||
}
|
||||
}
|
||||
|
||||
/// Normalizer for analysis report lines that maintains state internally.
|
||||
/// The first line is expected to be ReportMetadata, and subsequent lines
|
||||
/// are expected to be AnalysisRow entries.
|
||||
pub struct AnalysisLineNormalizer {
|
||||
is_first: bool,
|
||||
}
|
||||
|
||||
impl Default for AnalysisLineNormalizer {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl AnalysisLineNormalizer {
|
||||
pub fn new() -> Self {
|
||||
Self { is_first: true }
|
||||
}
|
||||
|
||||
/// Normalize a single line from an analysis report.
|
||||
/// Returns the normalized JSON string with a newline appended.
|
||||
pub fn normalize_line(&mut self, line: String) -> String {
|
||||
if self.is_first {
|
||||
self.is_first = false;
|
||||
// the first line is the report metadata. we overwrite the report version there to
|
||||
// latest, because the output of the remaining lines will follow latest versions
|
||||
if let Ok(mut metadata) = serde_json::from_str::<ReportMetadata>(&line) {
|
||||
metadata.normalize();
|
||||
serde_json::to_string(&metadata).unwrap_or(line) + "\n"
|
||||
} else {
|
||||
line + "\n"
|
||||
}
|
||||
} else {
|
||||
// Remaining lines are AnalysisRow, roundtrip them through serde to normalize them.
|
||||
if let Ok(row) = serde_json::from_str::<AnalysisRow>(&line) {
|
||||
serde_json::to_string(&row).unwrap_or(line) + "\n"
|
||||
} else {
|
||||
line + "\n"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
pub struct AnalysisRow {
|
||||
pub packet_timestamp: Option<DateTime<FixedOffset>>,
|
||||
@@ -128,12 +216,81 @@ impl AnalysisRow {
|
||||
}
|
||||
|
||||
pub fn contains_warnings(&self) -> bool {
|
||||
for event in self.events.iter().flatten() {
|
||||
if matches!(event.event_type, EventType::QualitativeWarning { .. }) {
|
||||
return true;
|
||||
}
|
||||
self.get_max_event_type() != EventType::Informational
|
||||
}
|
||||
|
||||
pub fn get_max_event_type(&self) -> EventType {
|
||||
self.events
|
||||
.iter()
|
||||
.flatten()
|
||||
.map(|event| event.event_type)
|
||||
.max()
|
||||
.unwrap_or(EventType::Informational)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for AnalysisRow {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
use serde::de::Error;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct V1AnalysisEntry {
|
||||
timestamp: DateTime<FixedOffset>,
|
||||
events: Vec<Option<Event>>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct V1Format {
|
||||
timestamp: DateTime<FixedOffset>,
|
||||
skipped_message_reasons: Vec<String>,
|
||||
analysis: Vec<V1AnalysisEntry>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct V2Format {
|
||||
packet_timestamp: Option<DateTime<FixedOffset>>,
|
||||
skipped_message_reason: Option<String>,
|
||||
events: Vec<Option<Event>>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum RowFormat {
|
||||
V1(V1Format),
|
||||
V2(V2Format),
|
||||
}
|
||||
|
||||
match RowFormat::deserialize(deserializer)? {
|
||||
RowFormat::V1(v1) => {
|
||||
// For v1 format, we can only deserialize the first non-skipped analysis entry
|
||||
// The caller needs to handle multiple rows differently for v1
|
||||
if let Some(first_analysis) = v1.analysis.first() {
|
||||
Ok(AnalysisRow {
|
||||
packet_timestamp: Some(first_analysis.timestamp),
|
||||
skipped_message_reason: None,
|
||||
events: first_analysis.events.clone(),
|
||||
})
|
||||
} else if let Some(first_reason) = v1.skipped_message_reasons.first() {
|
||||
Ok(AnalysisRow {
|
||||
packet_timestamp: Some(v1.timestamp),
|
||||
skipped_message_reason: Some(first_reason.clone()),
|
||||
events: Vec::new(),
|
||||
})
|
||||
} else {
|
||||
Err(D::Error::custom(
|
||||
"V1 format has no analysis entries or skipped reasons",
|
||||
))
|
||||
}
|
||||
}
|
||||
RowFormat::V2(v2) => Ok(AnalysisRow {
|
||||
packet_timestamp: v2.packet_timestamp,
|
||||
skipped_message_reason: v2.skipped_message_reason,
|
||||
events: v2.events,
|
||||
}),
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -293,3 +450,57 @@ impl Harness {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn test_analysis_row_deserialize_old_format() {
|
||||
let row: AnalysisRow = serde_json::from_value(json!({
|
||||
"packet_timestamp": "2023-01-01T00:00:00+00:00",
|
||||
"skipped_message_reason": null,
|
||||
"events": [
|
||||
{
|
||||
"event_type": { "type": "QualitativeWarning", "severity": "High" },
|
||||
"message": "Test warning"
|
||||
},
|
||||
{
|
||||
"event_type": { "type": "Informational" },
|
||||
"message": "Test info"
|
||||
},
|
||||
null
|
||||
]
|
||||
}))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(row.events[0].as_ref().unwrap().event_type, EventType::High);
|
||||
assert_eq!(
|
||||
row.events[1].as_ref().unwrap().event_type,
|
||||
EventType::Informational
|
||||
);
|
||||
assert!(row.events[2].is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_analysis_row_deserialize_new_format() {
|
||||
let row: AnalysisRow = serde_json::from_value(json!({
|
||||
"packet_timestamp": "2023-01-01T00:00:00+00:00",
|
||||
"skipped_message_reason": null,
|
||||
"events": [
|
||||
{ "event_type": "High", "message": "Test warning" },
|
||||
{ "event_type": "Informational", "message": "Test info" },
|
||||
null
|
||||
]
|
||||
}))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(row.events[0].as_ref().unwrap().event_type, EventType::High);
|
||||
assert_eq!(
|
||||
row.events[1].as_ref().unwrap().event_type,
|
||||
EventType::Informational
|
||||
);
|
||||
assert!(row.events[2].is_none());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use super::analyzer::{Analyzer, Event, EventType, Severity};
|
||||
use super::analyzer::{Analyzer, Event, EventType};
|
||||
use super::information_element::{InformationElement, LteInformationElement};
|
||||
use telcom_parser::lte_rrc::{
|
||||
DL_DCCH_MessageType, DL_DCCH_MessageType_c1, RRCConnectionReleaseCriticalExtensions,
|
||||
@@ -36,9 +36,7 @@ impl Analyzer for ConnectionRedirect2GDowngradeAnalyzer {
|
||||
{
|
||||
match carrier_info {
|
||||
RedirectedCarrierInfo::Geran(_carrier_freqs_geran) => Some(Event {
|
||||
event_type: EventType::QualitativeWarning {
|
||||
severity: Severity::High,
|
||||
},
|
||||
event_type: EventType::High,
|
||||
message: "Detected 2G downgrade".to_owned(),
|
||||
}),
|
||||
_ => Some(Event {
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::borrow::Cow;
|
||||
use pycrate_rs::nas::NASMessage;
|
||||
use pycrate_rs::nas::emm::EMMMessage;
|
||||
|
||||
use super::analyzer::{Analyzer, Event, EventType, Severity};
|
||||
use super::analyzer::{Analyzer, Event, EventType};
|
||||
use super::information_element::{InformationElement, LteInformationElement};
|
||||
use log::debug;
|
||||
|
||||
@@ -59,9 +59,7 @@ impl ImsiRequestedAnalyzer {
|
||||
// Unexpected IMSI without AttachRequest
|
||||
(current, State::IdentityRequest) if *current != State::AttachRequest => {
|
||||
self.flag = Some(Event {
|
||||
event_type: EventType::QualitativeWarning {
|
||||
severity: Severity::High,
|
||||
},
|
||||
event_type: EventType::High,
|
||||
message: format!(
|
||||
"Identity requested without Attach Request (frame {})",
|
||||
self.packet_num
|
||||
@@ -73,9 +71,7 @@ impl ImsiRequestedAnalyzer {
|
||||
// IMSI to Disconnect without AuthAccept
|
||||
(State::IdentityRequest, State::Disconnect) => {
|
||||
self.flag = Some(Event {
|
||||
event_type: EventType::QualitativeWarning {
|
||||
severity: Severity::High,
|
||||
},
|
||||
event_type: EventType::High,
|
||||
message: format!(
|
||||
"Disconnected after Identity Request without Auth Accept (frame {})",
|
||||
self.packet_num
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::borrow::Cow;
|
||||
|
||||
use telcom_parser::lte_rrc::{BCCH_DL_SCH_MessageType, BCCH_DL_SCH_MessageType_c1};
|
||||
|
||||
use super::analyzer::{Analyzer, Event, EventType, Severity};
|
||||
use super::analyzer::{Analyzer, Event, EventType};
|
||||
use super::information_element::{InformationElement, LteInformationElement};
|
||||
|
||||
pub struct IncompleteSibAnalyzer {
|
||||
@@ -44,9 +44,7 @@ impl Analyzer for IncompleteSibAnalyzer {
|
||||
&& sib1.scheduling_info_list.0.len() < 2
|
||||
{
|
||||
return Some(Event {
|
||||
event_type: EventType::QualitativeWarning {
|
||||
severity: Severity::Medium,
|
||||
},
|
||||
event_type: EventType::Medium,
|
||||
message: format!(
|
||||
"SIB1 scheduling info list was malformed (packet {})",
|
||||
self.packet_num
|
||||
|
||||
@@ -4,7 +4,7 @@ use pycrate_rs::nas::NASMessage;
|
||||
use pycrate_rs::nas::emm::EMMMessage;
|
||||
use pycrate_rs::nas::generated::emm::emm_security_mode_command::NASSecAlgoCiphAlgo::EPSEncryptionAlgorithmEEA0Null;
|
||||
|
||||
use super::analyzer::{Analyzer, Event, EventType, Severity};
|
||||
use super::analyzer::{Analyzer, Event, EventType};
|
||||
use super::information_element::{InformationElement, LteInformationElement};
|
||||
|
||||
pub struct NasNullCipherAnalyzer {
|
||||
@@ -52,9 +52,7 @@ impl Analyzer for NasNullCipherAnalyzer {
|
||||
&& req.nas_sec_algo.inner.ciph_algo == EPSEncryptionAlgorithmEEA0Null
|
||||
{
|
||||
return Some(Event {
|
||||
event_type: EventType::QualitativeWarning {
|
||||
severity: Severity::High,
|
||||
},
|
||||
event_type: EventType::High,
|
||||
message: format!(
|
||||
"NAS Security mode command requested null cipher(packet {})",
|
||||
self.packet_num
|
||||
|
||||
@@ -8,7 +8,7 @@ use telcom_parser::lte_rrc::{
|
||||
SecurityModeCommandCriticalExtensions, SecurityModeCommandCriticalExtensions_c1,
|
||||
};
|
||||
|
||||
use super::analyzer::{Analyzer, Event, EventType, Severity};
|
||||
use super::analyzer::{Analyzer, Event, EventType};
|
||||
use super::information_element::{InformationElement, LteInformationElement};
|
||||
|
||||
pub struct NullCipherAnalyzer {}
|
||||
@@ -153,9 +153,7 @@ impl Analyzer for NullCipherAnalyzer {
|
||||
};
|
||||
if null_cipher_detected {
|
||||
return Some(Event {
|
||||
event_type: EventType::QualitativeWarning {
|
||||
severity: Severity::High,
|
||||
},
|
||||
event_type: EventType::High,
|
||||
message: "Cell suggested use of null cipher".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use super::analyzer::{Analyzer, Event, EventType, Severity};
|
||||
use super::analyzer::{Analyzer, Event, EventType};
|
||||
use super::information_element::{InformationElement, LteInformationElement};
|
||||
use telcom_parser::lte_rrc::{
|
||||
BCCH_DL_SCH_MessageType, BCCH_DL_SCH_MessageType_c1, CellReselectionPriority,
|
||||
@@ -61,9 +61,7 @@ impl Analyzer for LteSib6And7DowngradeAnalyzer {
|
||||
&& p == 0
|
||||
{
|
||||
return Some(Event {
|
||||
event_type: EventType::QualitativeWarning {
|
||||
severity: Severity::High,
|
||||
},
|
||||
event_type: EventType::High,
|
||||
message:
|
||||
"LTE cell advertised a 3G cell for priority 0 reselection"
|
||||
.to_string(),
|
||||
@@ -78,9 +76,7 @@ impl Analyzer for LteSib6And7DowngradeAnalyzer {
|
||||
&& p == 0
|
||||
{
|
||||
return Some(Event {
|
||||
event_type: EventType::QualitativeWarning {
|
||||
severity: Severity::High,
|
||||
},
|
||||
event_type: EventType::High,
|
||||
message:
|
||||
"LTE cell advertised a 3G cell for priority 0 reselection"
|
||||
.to_string(),
|
||||
@@ -101,9 +97,7 @@ impl Analyzer for LteSib6And7DowngradeAnalyzer {
|
||||
&& p == 0
|
||||
{
|
||||
return Some(Event {
|
||||
event_type: EventType::QualitativeWarning {
|
||||
severity: Severity::High,
|
||||
},
|
||||
event_type: EventType::High,
|
||||
message: "LTE cell advertised a 2G cell for priority 0 reselection"
|
||||
.to_string(),
|
||||
});
|
||||
|
||||
+2
-2
@@ -1,10 +1,10 @@
|
||||
use serde::Serialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[cfg(target_family = "unix")]
|
||||
use nix::sys::utsname::uname;
|
||||
|
||||
/// Expose binary and system information.
|
||||
#[derive(Serialize, Debug)]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct RuntimeMetadata {
|
||||
/// The cargo package version from this library's cargo.toml, e.g., "1.2.3".
|
||||
pub rayhunter_version: String,
|
||||
|
||||
Reference in New Issue
Block a user