Esqueleto inicial: sabia-core + sabia-cli + UI local, OpenWolf

- sabia-core: descoberta de interfaces, controle de modo monitor/canal
  via iw (ponte até ter nl80211 nativo), captura crua 802.11 via
  AF_PACKET (sem libpcap), parsing de radiotap e quadros de
  gerenciamento (beacon/probe/EAPOL), agregação em tabela de APs/
  clientes, escrita de .pcap, trilha de auditoria encadeada por hash,
  auto-atualização via GitHub releases (curl, sem dependências de
  rede extras)
- sabia-cli: `sabia iface`, `sabia scan`, `sabia update`,
  `sabia audit-verify`, `sabia ui` (servidor local + navegador,
  protegido por token, mesma filosofia do Guará)
- OpenWolf inicializado para memória de projeto entre agentes
This commit is contained in:
Lucas committed 2026-10-23 08:22:04 -03:00
commit e989413018
76 files changed
+8386

No files matched your search

+216
View File
@@ -0,0 +1,216 @@
mod web;
use clap::{Parser, Subcommand};
use sabia_core::{audit::AuditLog, capture::RawCapture, iface, netlink, pcapwriter::PcapWriter, scan::ScanTable};
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;
#[derive(Parser)]
#[command(name = "sabia", about = "Sabiá — auditoria e monitoramento de redes Wi-Fi", version)]
struct Cli {
#[command(subcommand)]
cmd: Cmd,
}
#[derive(Subcommand)]
enum Cmd {
/// Lista interfaces de rede e mostra quais são Wi-Fi.
Iface {
#[command(subcommand)]
action: IfaceAction,
},
/// Varre o ar na interface (já em modo monitor) e mostra APs/clientes vistos.
Scan {
iface: String,
/// Canal fixo; sem isso, pula entre todos os canais suportados.
#[arg(long)]
channel: Option<u32>,
/// Para a varredura depois de N segundos (padrão: até Ctrl+C).
#[arg(long)]
seconds: Option<u64>,
/// Salva tudo que foi capturado em um .pcap (inclui handshakes).
#[arg(long)]
out: Option<PathBuf>,
},
/// Verifica e aplica atualizações do próprio sabia.
Update {
#[arg(long)]
apply: bool,
},
/// Confere a integridade da trilha de auditoria.
AuditVerify { path: PathBuf },
/// Abre a interface gráfica (servidor local + navegador).
Ui {
#[arg(long, default_value = "4337")]
port: u16,
},
}
#[derive(Subcommand)]
enum IfaceAction {
List,
/// Liga/desliga modo monitor numa interface Wi-Fi.
Monitor { iface: String, #[arg(value_enum)] state: OnOff },
}
#[derive(Clone, Copy, clap::ValueEnum)]
enum OnOff {
On,
Off,
}
fn audit_path() -> PathBuf {
PathBuf::from(".sabia-audit.log")
}
fn main() {
let cli = Cli::parse();
if let Err(e) = run(cli) {
eprintln!("erro: {e}");
std::process::exit(1);
}
}
fn run(cli: Cli) -> sabia_core::Result<()> {
match cli.cmd {
Cmd::Iface { action } => match action {
IfaceAction::List => {
for i in iface::list_all()? {
let tag = if i.phy.is_some() { "wifi" } else { "—" };
println!(
"{:<16} {:<18} {:<6} {:<10} {}",
i.name,
i.mac,
tag,
format!("{:?}", i.mode),
if i.up { "up" } else { "down" }
);
}
}
IfaceAction::Monitor { iface: name, state } => {
let enable = matches!(state, OnOff::On);
iface::get(&name)?; // valida que é Wi-Fi antes de tocar nela
netlink::set_monitor_mode(&name, enable)?;
let mut log = AuditLog::open(audit_path())?;
log.record(
"monitor_mode",
&name,
&format!("{{\"enable\": {enable}}}"),
)?;
println!("{name}: modo monitor {}", if enable { "ativado" } else { "desativado" });
}
},
Cmd::Scan { iface: name, channel, seconds, out } => {
let info = iface::get(&name)?;
if info.mode != iface::Mode::Monitor {
eprintln!("aviso: {name} não está em modo monitor (sabia iface monitor {name} on)");
}
let mut log = AuditLog::open(audit_path())?;
log.record("scan_start", &name, &format!("{{\"channel\": {channel:?}}}"))?;
let cap = RawCapture::open(&name)?;
cap.set_read_timeout(Duration::from_millis(500))?;
let running = Arc::new(AtomicBool::new(true));
let r = running.clone();
ctrlc::set_handler(move || r.store(false, Ordering::SeqCst))
.expect("falha ao instalar handler de Ctrl+C");
let mut table = ScanTable::new();
let mut writer = match &out {
Some(p) => Some(PcapWriter::create(p)?),
None => None,
};
let channels: Vec<u32> = match channel {
Some(c) => vec![c],
None => netlink::supported_channels(&name).unwrap_or_default(),
};
let mut chan_idx = 0usize;
let mut last_hop = std::time::Instant::now();
let deadline = seconds.map(|s| std::time::Instant::now() + Duration::from_secs(s));
let mut buf = [0u8; 65535];
println!("capturando em {name}... (Ctrl+C para parar)");
while running.load(Ordering::SeqCst) {
if let Some(d) = deadline {
if std::time::Instant::now() >= d {
break;
}
}
if channel.is_none() && !channels.is_empty() && last_hop.elapsed() > Duration::from_millis(300) {
chan_idx = (chan_idx + 1) % channels.len();
let _ = netlink::set_channel(&name, channels[chan_idx]);
last_hop = std::time::Instant::now();
}
match cap.recv(&mut buf)? {
Some(n) => {
let frame = &buf[..n];
let had_eapol = table.ingest(frame);
if had_eapol {
eprintln!("[!] handshake EAPOL visto");
}
if let Some(w) = writer.as_mut() {
w.write_frame(frame)?;
}
}
None => continue, // timeout, sem quadro
}
}
if let Some(w) = writer.as_mut() {
w.flush()?;
}
log.record("scan_stop", &name, &format!("{{\"aps_found\": {}}}", table.access_points().len()))?;
println!("\n{:<18} {:<24} {:<8} {:<8} {:<6} {}", "BSSID", "SSID", "CANAL", "SINAL", "SEG", "CLIENTES");
for ap in table.access_points() {
println!(
"{:<18} {:<24} {:<8} {:<8} {:<6} {}",
ap.bssid,
ap.ssid.unwrap_or_default(),
ap.channel.map(|c| c.to_string()).unwrap_or_default(),
ap.last_signal_dbm.map(|s| format!("{s}dBm")).unwrap_or_default(),
ap.security.unwrap_or_default(),
ap.clients.len()
);
}
}
Cmd::Update { apply } => {
let current = env!("CARGO_PKG_VERSION");
if apply {
let exe = std::env::current_exe()?;
let version = sabia_core::update::apply(&exe)?;
println!("atualizado para {version}");
} else {
let status = sabia_core::update::check(current)?;
if status.update_available {
println!(
"atualização disponível: {} -> {} (rode `sabia update --apply`)",
status.current_version,
status.latest_version.unwrap_or_default()
);
} else {
println!("já na última versão ({})", status.current_version);
}
}
}
Cmd::AuditVerify { path } => match sabia_core::audit::AuditLog::verify(&path)? {
None => println!("cadeia de auditoria íntegra"),
Some(idx) => println!("cadeia ROMPIDA na entrada {idx}"),
},
Cmd::Ui { port } => {
web::serve(port)?;
}
}
Ok(())
}