mirror of
https://github.com/EFForg/rayhunter.git
synced 2026-06-02 03:03:35 -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
+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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user