181 lines
5.1 KiB
Rust
181 lines
5.1 KiB
Rust
use std::io;
|
|
use std::time::Duration;
|
|
|
|
use crossterm::{
|
|
event::{self, Event, KeyCode, KeyEventKind},
|
|
execute,
|
|
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
|
};
|
|
use ratatui::{
|
|
backend::CrosstermBackend,
|
|
layout::{Constraint, Direction, Layout},
|
|
style::{Color, Modifier, Style},
|
|
text::{Line, Span},
|
|
widgets::{Block, Borders, Cell, Row, Table, TableState},
|
|
Terminal,
|
|
};
|
|
use tokio::sync::mpsc::Receiver;
|
|
|
|
use crate::alert::Alert;
|
|
|
|
const TICK_RATE: Duration = Duration::from_millis(16); // ~60 fps
|
|
|
|
/// Point d'entrée de la TUI. Bloque jusqu'à ce que l'utilisateur quitte.
|
|
pub fn run(mut rx: Receiver<Alert>) -> Result<(), Box<dyn std::error::Error>>
|
|
{
|
|
enable_raw_mode()?;
|
|
let mut stdout = io::stdout();
|
|
execute!(stdout, EnterAlternateScreen)?;
|
|
|
|
let backend = CrosstermBackend::new(stdout);
|
|
let mut terminal = Terminal::new(backend)?;
|
|
|
|
let mut alerts: Vec<Alert> = Vec::new();
|
|
let mut table_state = TableState::default();
|
|
|
|
let result = run_loop(&mut terminal, &mut rx, &mut alerts, &mut table_state);
|
|
|
|
// Restauration du terminal même en cas d'erreur
|
|
disable_raw_mode()?;
|
|
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
|
|
terminal.show_cursor()?;
|
|
|
|
result
|
|
}
|
|
|
|
fn run_loop(
|
|
terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
|
|
rx: &mut Receiver<Alert>,
|
|
alerts: &mut Vec<Alert>,
|
|
state: &mut TableState,
|
|
) -> Result<(), Box<dyn std::error::Error>>
|
|
{
|
|
loop
|
|
{
|
|
while let Ok(alert) = rx.try_recv()
|
|
{
|
|
alerts.push(alert);
|
|
state.select(Some(alerts.len().saturating_sub(1)));
|
|
}
|
|
|
|
terminal.draw(|frame| render(frame, alerts, state))?;
|
|
|
|
if event::poll(TICK_RATE)?
|
|
{
|
|
if let Event::Key(key) = event::read()?
|
|
{
|
|
if key.kind == KeyEventKind::Press
|
|
{
|
|
match key.code
|
|
{
|
|
KeyCode::Char('q') | KeyCode::Esc => return Ok(()),
|
|
KeyCode::Down => scroll_down(state, alerts.len()),
|
|
KeyCode::Up => scroll_up(state),
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn render(
|
|
frame: &mut ratatui::Frame,
|
|
alerts: &[Alert],
|
|
state: &mut TableState,
|
|
)
|
|
{
|
|
let area = frame.area();
|
|
|
|
let chunks = Layout::default()
|
|
.direction(Direction::Vertical)
|
|
.constraints([Constraint::Length(3), Constraint::Min(0)])
|
|
.split(area);
|
|
|
|
// Barre de titre
|
|
let title = Line::from(vec![
|
|
Span::styled(" Vauban v0.0.1 ", Style::default().add_modifier(Modifier::BOLD)),
|
|
Span::raw("— "),
|
|
Span::styled(
|
|
format!("{} alertes", alerts.len()),
|
|
Style::default().fg(Color::Yellow),
|
|
),
|
|
Span::raw(" "),
|
|
Span::styled(
|
|
"[↑↓] scroll [q] quitter",
|
|
Style::default().fg(Color::DarkGray),
|
|
),
|
|
]);
|
|
|
|
frame.render_widget(
|
|
Block::default().borders(Borders::ALL).title(title),
|
|
chunks[0],
|
|
);
|
|
|
|
// En-têtes de la table
|
|
let header = Row::new(
|
|
["Severity", "Rule", "PID", "Image", "CommandLine"].map(|h|
|
|
Cell::from(h).style(
|
|
Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD),
|
|
)
|
|
)
|
|
)
|
|
.height(1)
|
|
.bottom_margin(1);
|
|
|
|
// Coloration par sévérité
|
|
let severity_color = |s: &str| match s
|
|
{
|
|
"critical" => Color::Red,
|
|
"high" => Color::LightRed,
|
|
"medium" => Color::Yellow,
|
|
"low" => Color::Green,
|
|
_ => Color::White,
|
|
};
|
|
|
|
// Lignes
|
|
let rows: Vec<Row> = alerts
|
|
.iter()
|
|
.map(|a|
|
|
{
|
|
let color = severity_color(&a.severity);
|
|
Row::new([
|
|
Cell::from(a.severity.clone())
|
|
.style(Style::default().fg(color).add_modifier(Modifier::BOLD)),
|
|
Cell::from(a.rule.clone()),
|
|
Cell::from(a.event.get("ProcessId").cloned().unwrap_or_default()),
|
|
Cell::from(a.event.get("Image").cloned().unwrap_or_default()),
|
|
Cell::from(a.event.get("CommandLine").cloned().unwrap_or_default()),
|
|
])
|
|
})
|
|
.collect();
|
|
|
|
let table = Table::new(
|
|
rows,
|
|
[
|
|
Constraint::Length(10),
|
|
Constraint::Percentage(25),
|
|
Constraint::Length(8),
|
|
Constraint::Percentage(30),
|
|
Constraint::Min(0),
|
|
],
|
|
)
|
|
.header(header)
|
|
.block(Block::default().borders(Borders::ALL).title(" Alertes détectées "))
|
|
.row_highlight_style(Style::default().bg(Color::DarkGray));
|
|
|
|
frame.render_stateful_widget(table, chunks[1], state);
|
|
}
|
|
|
|
fn scroll_down(state: &mut TableState, len: usize)
|
|
{
|
|
if len == 0 { return; }
|
|
let next = state.selected().map(|i| (i + 1).min(len - 1)).unwrap_or(0);
|
|
state.select(Some(next));
|
|
}
|
|
|
|
fn scroll_up(state: &mut TableState)
|
|
{
|
|
let prev = state.selected().map(|i| i.saturating_sub(1)).unwrap_or(0);
|
|
state.select(Some(prev));
|
|
}
|