use crate::app::{AppState, GraveyardMode};
use crate::net::{Connection, ConnectionState};
use crate::theme::{
get_refresh_color, get_status_text, BLOOD_RED, BONE_WHITE, NEON_PURPLE, PUMPKIN_ORANGE,
TOXIC_GREEN,
};
use ratatui::{
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, BorderType, Borders, Paragraph, Sparkline},
Frame,
};
#[derive(Debug, Clone)]
pub struct SoulInspectorView {
pub target_name: String,
pub target_icon: String,
pub pid: Option<i32>,
#[allow(dead_code)]
pub ppid: Option<i32>,
#[allow(dead_code)]
pub user: Option<String>,
pub state_icon: String,
pub state_text: String,
pub state_color: Color,
pub refresh_ms: u64,
pub conn_count: usize,
pub server_count: usize,
pub client_count: usize,
pub public_count: usize,
pub sockets: Vec<SocketInfo>,
pub suspicious: bool,
pub suspicious_count: usize,
pub suspicious_reasons: Vec<String>,
pub tags: Vec<String>,
pub has_selection: bool,
}
#[derive(Debug, Clone)]
pub struct SocketInfo {
pub display: String,
pub remote: Option<String>,
pub state: ConnectionState,
}
impl Default for SoulInspectorView {
fn default() -> Self {
Self {
target_name: "No target selected".to_string(),
target_icon: "👻".to_string(),
pid: None,
ppid: None,
user: None,
state_icon: "⚪".to_string(),
state_text: "Idle".to_string(),
state_color: BONE_WHITE,
refresh_ms: 500,
conn_count: 0,
server_count: 0,
client_count: 0,
public_count: 0,
sockets: Vec::new(),
suspicious: false,
suspicious_count: 0,
suspicious_reasons: Vec::new(),
tags: Vec::new(),
has_selection: false,
}
}
}
pub fn build_soul_inspector_view(app: &AppState) -> SoulInspectorView {
let mut view = SoulInspectorView {
refresh_ms: app.refresh_config.refresh_ms,
..Default::default()
};
match app.graveyard_mode {
GraveyardMode::Host => {
if let Some(conn_idx) = app.selected_connection {
if let Some(conn) = app.connections.get(conn_idx) {
build_connection_view(&mut view, conn, &app.connections);
}
} else {
build_host_view(&mut view, &app.connections);
}
}
GraveyardMode::Process => {
if let Some(pid) = app.selected_process_pid {
build_process_view(&mut view, pid, &app.connections);
} else {
view.target_name = "No process selected".to_string();
view.target_icon = "❓".to_string();
}
}
}
view
}
fn build_host_view(view: &mut SoulInspectorView, connections: &[Connection]) {
view.target_name = "HOST".to_string();
view.target_icon = "🏠".to_string();
view.has_selection = true;
let established = connections
.iter()
.filter(|c| c.state == ConnectionState::Established)
.count();
let listening = connections
.iter()
.filter(|c| c.state == ConnectionState::Listen)
.count();
let other = connections.len() - established - listening;
let public_count = connections
.iter()
.filter(|c| is_public_ip(&c.remote_addr))
.count();
view.conn_count = connections.len();
view.server_count = listening;
view.client_count = established;
view.public_count = public_count;
let mut suspicious_count = 0;
let mut suspicious_reasons: Vec<String> = Vec::new();
for conn in connections {
if conn.remote_port > 49152 && conn.local_port > 49152 {
suspicious_count += 1;
if !suspicious_reasons.contains(&"high-port".to_string()) {
suspicious_reasons.push("high-port".to_string());
}
}
let standard_ports = [
80, 443, 22, 21, 25, 53, 110, 143, 993, 995, 3306, 5432, 6379, 27017,
];
if conn.state == ConnectionState::Established
&& !standard_ports.contains(&conn.remote_port)
&& conn.remote_port > 1024
&& is_public_ip(&conn.remote_addr)
{
suspicious_count += 1;
if !suspicious_reasons.contains(&"non-standard".to_string()) {
suspicious_reasons.push("non-standard".to_string());
}
}
}
view.suspicious = suspicious_count > 0;
view.suspicious_count = suspicious_count;
view.suspicious_reasons = suspicious_reasons;
if connections.is_empty() {
view.state_icon = "⚪".to_string();
view.state_text = "No connections".to_string();
view.state_color = BONE_WHITE;
} else if established > 0 {
view.state_icon = "🟢".to_string();
view.state_text = format!("{} active, {} listening", established, listening);
view.state_color = TOXIC_GREEN;
} else if listening > 0 {
view.state_icon = "🟡".to_string();
view.state_text = format!("{} listening", listening);
view.state_color = PUMPKIN_ORANGE;
} else {
view.state_icon = "🟠".to_string();
view.state_text = format!("{} other states", other);
view.state_color = PUMPKIN_ORANGE;
}
view.sockets = connections
.iter()
.take(5)
.map(connection_to_socket_info)
.collect();
if listening > 0 {
view.tags.push(format!("server ({})", listening));
}
if established > 0 {
view.tags.push(format!("client ({})", established));
}
}
fn build_connection_view(
view: &mut SoulInspectorView,
conn: &Connection,
all_connections: &[Connection],
) {
view.has_selection = true;
view.target_icon = "🔗".to_string();
if conn.state == ConnectionState::Listen {
view.target_name = format!("{}:{}", conn.local_addr, conn.local_port);
} else {
view.target_name = format!("{}:{}", conn.remote_addr, conn.remote_port);
}
if view.target_name.len() > 20 {
view.target_name = format!("{}...", &view.target_name[..17]);
}
view.pid = conn.pid;
let (icon, text, color) = connection_state_display(conn.state);
view.state_icon = icon;
view.state_text = text;
view.state_color = color;
if conn.state != ConnectionState::Listen {
view.conn_count = all_connections
.iter()
.filter(|c| c.remote_addr == conn.remote_addr)
.count();
} else {
view.conn_count = 1;
}
view.sockets = vec![connection_to_socket_info(conn)];
if let Some(ref name) = conn.process_name {
view.tags.push(name.clone());
}
check_suspicious_patterns(view, conn);
}
fn build_process_view(view: &mut SoulInspectorView, pid: i32, connections: &[Connection]) {
view.has_selection = true;
view.target_icon = "⚰️".to_string();
view.pid = Some(pid);
let process_conns: Vec<&Connection> =
connections.iter().filter(|c| c.pid == Some(pid)).collect();
let process_name = process_conns
.iter()
.find_map(|c| c.process_name.clone())
.unwrap_or_else(|| format!("PID {}", pid));
view.target_name = if process_name.len() > 15 {
format!("{}...", &process_name[..12])
} else {
process_name.clone()
};
view.conn_count = process_conns.len();
let established = process_conns
.iter()
.filter(|c| c.state == ConnectionState::Established)
.count();
let listening = process_conns
.iter()
.filter(|c| c.state == ConnectionState::Listen)
.count();
let problematic = process_conns
.iter()
.filter(|c| {
matches!(
c.state,
ConnectionState::CloseWait | ConnectionState::TimeWait | ConnectionState::Close
)
})
.count();
if process_conns.is_empty() {
view.state_icon = "⚪".to_string();
view.state_text = "No connections".to_string();
view.state_color = BONE_WHITE;
} else if problematic > 0 {
view.state_icon = "🟠".to_string();
view.state_text = format!("{} problematic", problematic);
view.state_color = PUMPKIN_ORANGE;
} else if established > 0 {
view.state_icon = "🟢".to_string();
view.state_text = format!("{} established", established);
view.state_color = TOXIC_GREEN;
} else if listening > 0 {
view.state_icon = "🟡".to_string();
view.state_text = format!("{} listening", listening);
view.state_color = PUMPKIN_ORANGE;
} else {
view.state_icon = "⚪".to_string();
view.state_text = "Idle".to_string();
view.state_color = BONE_WHITE;
}
view.sockets = process_conns
.iter()
.take(5)
.map(|c| connection_to_socket_info(c))
.collect();
view.tags.push(process_name);
if listening > 0 {
view.tags.push("server".to_string());
}
if established > 0 {
view.tags.push("client".to_string());
}
}
fn connection_to_socket_info(conn: &Connection) -> SocketInfo {
let display = format!("tcp://{}:{}", conn.local_addr, conn.local_port);
let remote = if conn.state == ConnectionState::Listen || conn.remote_addr == "0.0.0.0" {
None
} else {
Some(format!("{}:{}", conn.remote_addr, conn.remote_port))
};
SocketInfo {
display,
remote,
state: conn.state,
}
}
fn connection_state_display(state: ConnectionState) -> (String, String, Color) {
match state {
ConnectionState::Established => (
"🟢".to_string(),
"ESTABLISHED (Alive)".to_string(),
TOXIC_GREEN,
),
ConnectionState::Listen => (
"🟡".to_string(),
"LISTEN (Waiting)".to_string(),
PUMPKIN_ORANGE,
),
ConnectionState::TimeWait => (
"🟠".to_string(),
"TIME_WAIT (Closing)".to_string(),
PUMPKIN_ORANGE,
),
ConnectionState::CloseWait => (
"🟠".to_string(),
"CLOSE_WAIT (Stale)".to_string(),
PUMPKIN_ORANGE,
),
ConnectionState::Close => ("🔴".to_string(), "CLOSED (Dead)".to_string(), BLOOD_RED),
ConnectionState::SynSent => (
"🟡".to_string(),
"SYN_SENT (Connecting)".to_string(),
PUMPKIN_ORANGE,
),
ConnectionState::SynRecv => (
"🟡".to_string(),
"SYN_RECV (Handshake)".to_string(),
PUMPKIN_ORANGE,
),
ConnectionState::FinWait1 | ConnectionState::FinWait2 => (
"🟠".to_string(),
"FIN_WAIT (Closing)".to_string(),
PUMPKIN_ORANGE,
),
ConnectionState::LastAck => (
"🟠".to_string(),
"LAST_ACK (Closing)".to_string(),
PUMPKIN_ORANGE,
),
ConnectionState::Closing => ("🟠".to_string(), "CLOSING".to_string(), PUMPKIN_ORANGE),
ConnectionState::Unknown => ("⚪".to_string(), "UNKNOWN".to_string(), BONE_WHITE),
}
}
fn check_suspicious_patterns(view: &mut SoulInspectorView, conn: &Connection) {
if conn.remote_port > 49152 && conn.local_port > 49152 {
view.suspicious = true;
view.tags.push("high-port".to_string());
}
let standard_ports = [
80, 443, 22, 21, 25, 53, 110, 143, 993, 995, 3306, 5432, 6379, 27017,
];
if conn.state == ConnectionState::Established
&& !standard_ports.contains(&conn.remote_port)
&& conn.remote_port > 1024
{
view.tags.push("non-standard".to_string());
}
}
fn is_public_ip(addr: &str) -> bool {
if addr == "127.0.0.1" || addr == "::1" || addr == "0.0.0.0" || addr.starts_with("127.") {
return false;
}
if addr.starts_with("10.") || addr.starts_with("192.168.") {
return false;
}
if addr.starts_with("172.") {
if let Some(second_octet) = addr.split('.').nth(1) {
if let Ok(octet) = second_octet.parse::<u8>() {
if (16..=31).contains(&octet) {
return false;
}
}
}
}
if addr.starts_with("169.254.") || addr.starts_with("fe80:") {
return false;
}
true
}
pub fn render_soul_inspector(f: &mut Frame, area: Rect, app: &AppState) {
let view = build_soul_inspector_view(app);
let inspector_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(11), Constraint::Length(5), Constraint::Min(0), ])
.split(area);
let recently_changed = app
.refresh_config
.last_change
.map(|last| last.elapsed() < crate::app::CHANGE_HIGHLIGHT_DURATION)
.unwrap_or(false);
let refresh_color = get_refresh_color(view.refresh_ms, 100, recently_changed);
let refresh_style = if recently_changed {
Style::default()
.fg(refresh_color)
.add_modifier(Modifier::BOLD | Modifier::UNDERLINED)
} else {
Style::default().fg(refresh_color)
};
let overdrive_enabled = app.graveyard_settings.overdrive_enabled;
let overdrive_suffix = if overdrive_enabled {
get_status_text(ConnectionState::Established, true).to_string()
} else {
"Active".to_string()
};
let status_display = format!(
"{} {} ({})",
view.state_icon,
view.state_text
.split(" (")
.next()
.unwrap_or(&view.state_text),
overdrive_suffix
);
let suspicious_indicator = if view.suspicious {
Span::styled(
" ⚠️",
Style::default().fg(BLOOD_RED).add_modifier(Modifier::BOLD),
)
} else {
Span::raw("")
};
let mut top_content = vec![
Line::from(vec![
Span::styled(" TARGET: ", Style::default().fg(Color::DarkGray)),
Span::styled(
format!("{} {}", view.target_icon, view.target_name),
Style::default()
.fg(PUMPKIN_ORANGE)
.add_modifier(Modifier::BOLD),
),
suspicious_indicator,
]),
Line::from(vec![
Span::styled(" ROLE: ", Style::default().fg(Color::DarkGray)),
Span::styled(
format!("[server {}] ", view.server_count),
Style::default().fg(NEON_PURPLE),
),
Span::styled(
format!("[client {}] ", view.client_count),
Style::default().fg(TOXIC_GREEN),
),
Span::styled(
format!("[public {}]", view.public_count),
Style::default().fg(PUMPKIN_ORANGE),
),
]),
Line::from(vec![
Span::styled(" STATE: ", Style::default().fg(Color::DarkGray)),
Span::styled(
status_display,
Style::default()
.fg(view.state_color)
.add_modifier(Modifier::BOLD),
),
]),
Line::from(vec![
Span::styled(" CONN: ", Style::default().fg(Color::DarkGray)),
Span::styled(
format!("{} total", view.conn_count),
Style::default().fg(BONE_WHITE),
),
if let Some(pid) = view.pid {
Span::styled(
format!(" (PID: {})", pid),
Style::default().fg(Color::Cyan),
)
} else {
Span::raw("")
},
]),
];
if view.suspicious {
let reasons = if view.suspicious_reasons.is_empty() {
"unknown".to_string()
} else {
view.suspicious_reasons.join(", ")
};
top_content.push(Line::from(vec![
Span::styled(" RISK: ", Style::default().fg(Color::DarkGray)),
Span::styled("🩸 ", Style::default().fg(BLOOD_RED)),
Span::styled(
format!("{} suspicious ({})", view.suspicious_count, reasons),
Style::default().fg(BLOOD_RED).add_modifier(Modifier::BOLD),
),
]));
}
top_content.push(Line::from(vec![
Span::styled(" BPF: ", Style::default().fg(Color::DarkGray)),
Span::styled("ACTIVE ", Style::default().fg(TOXIC_GREEN)),
Span::styled("(", Style::default().fg(Color::DarkGray)),
Span::styled(format!("{}ms", view.refresh_ms), refresh_style),
Span::styled(")", Style::default().fg(Color::DarkGray)),
]));
let title_spans = if view.suspicious {
vec![
Span::styled(
"━ 🔮 Soul Inspector ",
Style::default()
.fg(NEON_PURPLE)
.add_modifier(Modifier::BOLD),
),
Span::styled(
"⚠️ ",
Style::default().fg(BLOOD_RED).add_modifier(Modifier::BOLD),
),
Span::styled("━━━━", Style::default().fg(NEON_PURPLE)),
]
} else {
vec![
Span::styled(
"━ 🔮 Soul Inspector (Detail) ",
Style::default()
.fg(NEON_PURPLE)
.add_modifier(Modifier::BOLD),
),
Span::styled("━━━━━━", Style::default().fg(NEON_PURPLE)),
]
};
let top_paragraph = Paragraph::new(top_content).block(
Block::default()
.title(title_spans)
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(if view.suspicious {
BLOOD_RED
} else {
NEON_PURPLE
})),
);
f.render_widget(top_paragraph, inspector_chunks[0]);
let traffic_avg = if app.traffic_history.is_empty() {
0.0
} else {
app.traffic_history.iter().sum::<u64>() as f64 / app.traffic_history.len() as f64
};
let traffic_peak = app.traffic_history.iter().max().copied().unwrap_or(0);
let sparkline = Sparkline::default()
.block(
Block::default()
.title(vec![
Span::styled(
" 📊 Activity ",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
),
Span::styled(
format!("Avg:{:.0} ", traffic_avg),
Style::default().fg(BONE_WHITE),
),
Span::styled(
format!("Peak:{} ", traffic_peak),
Style::default().fg(PUMPKIN_ORANGE),
),
])
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(NEON_PURPLE)),
)
.data(&app.traffic_history)
.style(Style::default().fg(TOXIC_GREEN))
.max(100);
f.render_widget(sparkline, inspector_chunks[1]);
let mut socket_lines = vec![Line::from("")];
if view.sockets.is_empty() {
socket_lines.push(Line::from(vec![Span::styled(
" (no sockets)",
Style::default()
.fg(Color::DarkGray)
.add_modifier(Modifier::ITALIC),
)]));
} else {
for socket in &view.sockets {
let state_color = match socket.state {
ConnectionState::Established => TOXIC_GREEN,
ConnectionState::Listen => PUMPKIN_ORANGE,
ConnectionState::TimeWait | ConnectionState::CloseWait => PUMPKIN_ORANGE,
ConnectionState::Close => BLOOD_RED,
_ => BONE_WHITE,
};
let state_str = match socket.state {
ConnectionState::Established => "ESTABLISHED",
ConnectionState::Listen => "LISTEN",
ConnectionState::TimeWait => "TIME_WAIT",
ConnectionState::CloseWait => "CLOSE_WAIT",
ConnectionState::Close => "CLOSED",
ConnectionState::SynSent => "SYN_SENT",
_ => "OTHER",
};
if let Some(ref remote) = socket.remote {
socket_lines.push(Line::from(vec![
Span::raw(" > "),
Span::styled(&socket.display, Style::default().fg(Color::Cyan)),
Span::raw(" → "),
Span::styled(remote, Style::default().fg(Color::Blue)),
]));
} else {
socket_lines.push(Line::from(vec![
Span::raw(" > "),
Span::styled(&socket.display, Style::default().fg(Color::Cyan)),
Span::styled(
format!(" ({})", state_str),
Style::default().fg(state_color),
),
]));
}
}
if view.conn_count > view.sockets.len() {
socket_lines.push(Line::from(vec![Span::styled(
format!(" ... and {} more", view.conn_count - view.sockets.len()),
Style::default()
.fg(Color::DarkGray)
.add_modifier(Modifier::ITALIC),
)]));
}
}
let socket_paragraph = Paragraph::new(socket_lines).block(
Block::default()
.title(vec![Span::styled(
format!(" 📜 Open Sockets ({}) ", view.sockets.len()),
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
)])
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(NEON_PURPLE)),
);
f.render_widget(socket_paragraph, inspector_chunks[2]);
}