v2.0.0: adaptive eBPF firewall with AI honeypot and P2P threat mesh

This commit is contained in:
Vladyslav Soliannikov 2026-04-07 22:28:11 +00:00
commit 37c6bbf5a1
133 changed files with 28073 additions and 0 deletions

17
hivemind-dashboard/Cargo.toml Executable file
View file

@ -0,0 +1,17 @@
[package]
name = "hivemind-dashboard"
version = "0.1.0"
edition = "2021"
description = "Live TUI dashboard for HiveMind mesh monitoring"
[dependencies]
common = { path = "../common", default-features = false, features = ["user"] }
tokio = { workspace = true }
hyper = { workspace = true }
http-body-util = { workspace = true }
hyper-util = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
anyhow = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }

345
hivemind-dashboard/src/app.rs Executable file
View file

@ -0,0 +1,345 @@
//! ANSI terminal dashboard renderer.
//!
//! Renders a live-updating dashboard using only ANSI escape codes.
//! No external TUI crates — pure `\x1B[` sequences.
use std::io::{self, Write};
use crate::collector::MeshStats;
/// ANSI color codes.
const GREEN: &str = "\x1B[32m";
const YELLOW: &str = "\x1B[33m";
const RED: &str = "\x1B[31m";
const CYAN: &str = "\x1B[36m";
const BOLD: &str = "\x1B[1m";
const DIM: &str = "\x1B[2m";
const RESET: &str = "\x1B[0m";
/// Unicode box-drawing characters.
const TL: char = '┌';
const TR: char = '┐';
const BL: char = '└';
const BR: char = '┘';
const HZ: char = '─';
const VT: char = '│';
/// Terminal dashboard state.
pub struct Dashboard {
stats: MeshStats,
frame: u64,
}
impl Dashboard {
pub fn new() -> Self {
Self {
stats: MeshStats::default(),
frame: 0,
}
}
/// Update dashboard with fresh stats.
pub fn update(&mut self, stats: MeshStats) {
self.stats = stats;
self.frame += 1;
}
/// Render the full dashboard to the given writer.
pub fn render<W: Write>(&self, w: &mut W) -> io::Result<()> {
// Move cursor to top-left, clear screen
write!(w, "\x1B[H\x1B[2J")?;
self.render_header(w)?;
writeln!(w)?;
self.render_mesh_panel(w)?;
writeln!(w)?;
self.render_threat_panel(w)?;
writeln!(w)?;
self.render_network_fw_panel(w)?;
writeln!(w)?;
self.render_a2a_panel(w)?;
writeln!(w)?;
self.render_crypto_panel(w)?;
writeln!(w)?;
self.render_footer(w)?;
w.flush()
}
fn render_header<W: Write>(&self, w: &mut W) -> io::Result<()> {
let status = if self.stats.connected {
format!("{GREEN}● CONNECTED{RESET}")
} else {
format!("{RED}○ DISCONNECTED{RESET}")
};
writeln!(
w,
" {BOLD}{CYAN}╔══════════════════════════════════════════════════╗{RESET}"
)?;
writeln!(
w,
" {BOLD}{CYAN}║{RESET} {BOLD}BLACKWALL HIVEMIND{RESET} {DIM}v2.0{RESET} \
{status} {DIM}frame #{}{RESET} {BOLD}{CYAN}{RESET}",
self.frame,
)?;
writeln!(
w,
" {BOLD}{CYAN}╚══════════════════════════════════════════════════╝{RESET}"
)
}
fn render_mesh_panel<W: Write>(&self, w: &mut W) -> io::Result<()> {
self.draw_box_top(w, " P2P Mesh ", 48)?;
self.draw_kv(w, "Peers", &self.stats.peer_count.to_string(), 48)?;
self.draw_kv(
w,
"DHT Records",
&self.stats.dht_records.to_string(),
48,
)?;
self.draw_kv(
w,
"GossipSub Topics",
&self.stats.gossip_topics.to_string(),
48,
)?;
self.draw_kv(
w,
"Messages/s",
&format!("{:.1}", self.stats.messages_per_sec),
48,
)?;
self.draw_box_bottom(w, 48)
}
fn render_threat_panel<W: Write>(&self, w: &mut W) -> io::Result<()> {
self.draw_box_top(w, " Threat Intel ", 48)?;
self.draw_kv(
w,
"IoCs Shared",
&self.stats.iocs_shared.to_string(),
48,
)?;
self.draw_kv(
w,
"IoCs Received",
&self.stats.iocs_received.to_string(),
48,
)?;
let reputation_color = if self.stats.avg_reputation > 80.0 {
GREEN
} else if self.stats.avg_reputation > 50.0 {
YELLOW
} else {
RED
};
self.draw_kv_colored(
w,
"Avg Reputation",
&format!("{:.1}", self.stats.avg_reputation),
reputation_color,
48,
)?;
self.draw_box_bottom(w, 48)
}
fn render_a2a_panel<W: Write>(&self, w: &mut W) -> io::Result<()> {
self.draw_box_top(w, " A2A Firewall ", 48)?;
self.draw_kv(
w,
"JWTs Verified",
&self.stats.a2a_jwts_verified.to_string(),
48,
)?;
self.draw_kv(
w,
"Violations Blocked",
&self.stats.a2a_violations.to_string(),
48,
)?;
self.draw_kv(
w,
"Injections Detected",
&self.stats.a2a_injections.to_string(),
48,
)?;
self.draw_box_bottom(w, 48)
}
fn render_network_fw_panel<W: Write>(&self, w: &mut W) -> io::Result<()> {
self.draw_box_top(w, " Network Firewall ", 48)?;
self.draw_kv(
w,
"Packets Total",
&self.stats.packets_total.to_string(),
48,
)?;
self.draw_kv(
w,
"Packets Passed",
&self.stats.packets_passed.to_string(),
48,
)?;
let dropped_color = if self.stats.packets_dropped > 0 {
RED
} else {
GREEN
};
self.draw_kv_colored(
w,
"Packets Dropped",
&self.stats.packets_dropped.to_string(),
dropped_color,
48,
)?;
let anomaly_color = if self.stats.anomalies_sent > 0 {
YELLOW
} else {
GREEN
};
self.draw_kv_colored(
w,
"Anomalies",
&self.stats.anomalies_sent.to_string(),
anomaly_color,
48,
)?;
self.draw_box_bottom(w, 48)
}
fn render_crypto_panel<W: Write>(&self, w: &mut W) -> io::Result<()> {
self.draw_box_top(w, " Cryptography ", 48)?;
self.draw_kv(
w,
"ZKP Proofs Generated",
&self.stats.zkp_proofs_generated.to_string(),
48,
)?;
self.draw_kv(
w,
"ZKP Proofs Verified",
&self.stats.zkp_proofs_verified.to_string(),
48,
)?;
let fhe_status = if self.stats.fhe_encrypted {
format!("{GREEN}AES-256-GCM{RESET}")
} else {
format!("{YELLOW}STUB{RESET}")
};
self.draw_kv(w, "FHE Mode", &fhe_status, 48)?;
self.draw_box_bottom(w, 48)
}
fn render_footer<W: Write>(&self, w: &mut W) -> io::Result<()> {
writeln!(
w,
" {DIM}Press Ctrl+C to exit | Refresh: 1s | \
API: hivemind-api{RESET}"
)
}
// ── Box drawing helpers ─────────────────────────────────────
fn draw_box_top<W: Write>(
&self,
w: &mut W,
title: &str,
width: usize,
) -> io::Result<()> {
let inner = width - 2 - title.len();
write!(w, " {TL}{HZ}")?;
write!(w, "{BOLD}{title}{RESET}")?;
for _ in 0..inner {
write!(w, "{HZ}")?;
}
writeln!(w, "{TR}")
}
fn draw_box_bottom<W: Write>(&self, w: &mut W, width: usize) -> io::Result<()> {
write!(w, " {BL}")?;
for _ in 0..width - 2 {
write!(w, "{HZ}")?;
}
writeln!(w, "{BR}")
}
fn draw_kv<W: Write>(
&self,
w: &mut W,
key: &str,
value: &str,
width: usize,
) -> io::Result<()> {
let padding = width - 6 - key.len() - value.len();
let pad = if padding > 0 { padding } else { 1 };
write!(w, " {VT} {key}")?;
for _ in 0..pad {
write!(w, " ")?;
}
writeln!(w, "{BOLD}{value}{RESET} {VT}")
}
fn draw_kv_colored<W: Write>(
&self,
w: &mut W,
key: &str,
value: &str,
color: &str,
width: usize,
) -> io::Result<()> {
let padding = width - 6 - key.len() - value.len();
let pad = if padding > 0 { padding } else { 1 };
write!(w, " {VT} {key}")?;
for _ in 0..pad {
write!(w, " ")?;
}
writeln!(w, "{color}{BOLD}{value}{RESET} {VT}")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn render_default_dashboard() {
let dash = Dashboard::new();
let mut buf = Vec::new();
dash.render(&mut buf).expect("render");
let output = String::from_utf8(buf).expect("utf8");
assert!(output.contains("BLACKWALL HIVEMIND"));
assert!(output.contains("P2P Mesh"));
assert!(output.contains("Threat Intel"));
assert!(output.contains("Network Firewall"));
assert!(output.contains("A2A Firewall"));
assert!(output.contains("Cryptography"));
}
#[test]
fn update_increments_frame() {
let mut dash = Dashboard::new();
assert_eq!(dash.frame, 0);
dash.update(MeshStats::default());
assert_eq!(dash.frame, 1);
dash.update(MeshStats::default());
assert_eq!(dash.frame, 2);
}
#[test]
fn connected_shows_green() {
let mut dash = Dashboard::new();
dash.update(MeshStats {
connected: true,
..Default::default()
});
let mut buf = Vec::new();
dash.render(&mut buf).expect("render");
let output = String::from_utf8(buf).expect("utf8");
assert!(output.contains("CONNECTED"));
}
}

View file

@ -0,0 +1,150 @@
//! Stats collector — polls hivemind-api for mesh statistics.
use serde::Deserialize;
use tracing::warn;
/// Collected mesh statistics for dashboard rendering.
#[derive(Debug, Clone, Default, Deserialize)]
pub struct MeshStats {
/// Whether we're connected to the API.
#[serde(default)]
pub connected: bool,
// P2P Mesh
#[serde(default)]
pub peer_count: u64,
#[serde(default)]
pub dht_records: u64,
#[serde(default)]
pub gossip_topics: u64,
#[serde(default)]
pub messages_per_sec: f64,
// Threat Intel
#[serde(default)]
pub iocs_shared: u64,
#[serde(default)]
pub iocs_received: u64,
#[serde(default)]
pub avg_reputation: f64,
// Network Firewall (XDP/eBPF)
#[serde(default)]
pub packets_total: u64,
#[serde(default)]
pub packets_passed: u64,
#[serde(default)]
pub packets_dropped: u64,
#[serde(default)]
pub anomalies_sent: u64,
// A2A Firewall (separate counters)
#[serde(default)]
pub a2a_jwts_verified: u64,
#[serde(default)]
pub a2a_violations: u64,
#[serde(default)]
pub a2a_injections: u64,
// Cryptography
#[serde(default)]
pub zkp_proofs_generated: u64,
#[serde(default)]
pub zkp_proofs_verified: u64,
#[serde(default)]
pub fhe_encrypted: bool,
}
/// HTTP collector that polls the hivemind-api stats endpoint.
pub struct Collector {
url: String,
}
impl Collector {
/// Create a new collector targeting the given base URL.
pub fn new(base_url: &str) -> Self {
Self {
url: format!("{}/stats", base_url.trim_end_matches('/')),
}
}
/// Fetch latest stats from the API.
///
/// Returns default stats with `connected = false` on any error.
pub async fn fetch(&self) -> MeshStats {
match self.fetch_inner().await {
Ok(mut stats) => {
stats.connected = true;
stats
}
Err(e) => {
warn!(url = %self.url, error = %e, "failed to fetch mesh stats");
MeshStats {
connected: false,
..Default::default()
}
}
}
}
async fn fetch_inner(&self) -> Result<MeshStats, Box<dyn std::error::Error>> {
// Use a simple TCP connection + manual HTTP/1.1 request
// to avoid pulling in heavy HTTP client deps.
let url: hyper::Uri = self.url.parse()?;
let host = url.host().unwrap_or("127.0.0.1");
let port = url.port_u16().unwrap_or(9100);
let path = url.path();
let stream = tokio::net::TcpStream::connect(format!("{host}:{port}")).await?;
let io = hyper_util::rt::TokioIo::new(stream);
let (mut sender, conn) = hyper::client::conn::http1::handshake(io).await?;
tokio::spawn(async move {
if let Err(e) = conn.await {
warn!(error = %e, "HTTP connection error");
}
});
let req = hyper::Request::builder()
.uri(path)
.header("Host", host)
.body(http_body_util::Empty::<hyper::body::Bytes>::new())?;
let resp = sender.send_request(req).await?;
use http_body_util::BodyExt;
let body = resp.into_body().collect().await?.to_bytes();
let stats: MeshStats = serde_json::from_slice(&body)?;
Ok(stats)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_stats_disconnected() {
let stats = MeshStats::default();
assert!(!stats.connected);
assert_eq!(stats.peer_count, 0);
}
#[test]
fn deserialize_partial_json() {
let json = r#"{"peer_count": 42, "fhe_encrypted": true}"#;
let stats: MeshStats = serde_json::from_str(json).expect("parse");
assert_eq!(stats.peer_count, 42);
assert!(stats.fhe_encrypted);
assert_eq!(stats.a2a_jwts_verified, 0); // default
}
#[test]
fn collector_url_construction() {
let c1 = Collector::new("http://localhost:9100");
assert_eq!(c1.url, "http://localhost:9100/stats");
let c2 = Collector::new("http://localhost:9100/");
assert_eq!(c2.url, "http://localhost:9100/stats");
}
}

76
hivemind-dashboard/src/main.rs Executable file
View file

@ -0,0 +1,76 @@
//! HiveMind TUI Dashboard — live mesh monitoring via ANSI terminal.
//!
//! Zero external TUI deps — pure ANSI escape codes + tokio.
//! Polls the hivemind-api HTTP endpoint for stats and renders
//! a live-updating dashboard in the terminal.
use std::io::{self, Write};
use std::time::Duration;
use tokio::signal;
use tracing_subscriber::EnvFilter;
mod app;
mod collector;
use app::Dashboard;
use collector::Collector;
/// Default refresh interval in milliseconds.
const DEFAULT_REFRESH_MS: u64 = 1000;
/// Default API endpoint (hivemind-api default port).
const DEFAULT_API_URL: &str = "http://127.0.0.1:8090";
#[tokio::main(flavor = "current_thread")]
async fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env())
.with_target(false)
.init();
let api_url = std::env::args()
.nth(1)
.unwrap_or_else(|| DEFAULT_API_URL.to_string());
let refresh = Duration::from_millis(
std::env::args()
.nth(2)
.and_then(|s| s.parse().ok())
.unwrap_or(DEFAULT_REFRESH_MS),
);
let collector = Collector::new(&api_url);
let mut dashboard = Dashboard::new();
// Enter alternate screen + hide cursor
print!("\x1B[?1049h\x1B[?25l");
io::stdout().flush()?;
let result = run_loop(&collector, &mut dashboard, refresh).await;
// Restore terminal: show cursor + leave alternate screen
print!("\x1B[?25h\x1B[?1049l");
io::stdout().flush()?;
result
}
async fn run_loop(
collector: &Collector,
dashboard: &mut Dashboard,
refresh: Duration,
) -> anyhow::Result<()> {
loop {
tokio::select! {
_ = signal::ctrl_c() => {
break;
}
_ = tokio::time::sleep(refresh) => {
let stats = collector.fetch().await;
dashboard.update(stats);
dashboard.render(&mut io::stdout())?;
}
}
}
Ok(())
}