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

+112
View File
@@ -0,0 +1,112 @@
//! Agregação de quadros capturados em uma tabela de APs e clientes vistos,
//! no espírito do `airodump-ng` — mas como uma struct reaproveitável tanto
//! pela CLI quanto pela interface gráfica.
use crate::dot11::{self, FrameKind, Security};
use crate::radiotap;
use std::collections::HashMap;
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Debug, Clone, serde::Serialize)]
pub struct AccessPoint {
pub bssid: String,
pub ssid: Option<String>,
pub channel: Option<u32>,
pub security: Option<String>,
pub last_signal_dbm: Option<i8>,
pub beacons_seen: u64,
pub clients: Vec<String>,
pub last_seen_unix: u64,
}
#[derive(Default)]
pub struct ScanTable {
aps: HashMap<String, AccessPoint>,
}
impl ScanTable {
pub fn new() -> Self {
Self::default()
}
/// Processa um quadro radiotap+802.11 crú, atualizando a tabela.
/// Retorna `true` se o quadro continha um pacote EAPOL (indício de
/// handshake WPA em andamento — útil para o chamador decidir salvar em
/// pcap).
pub fn ingest(&mut self, raw: &[u8]) -> bool {
let rt = match radiotap::parse(raw) {
Some(rt) => rt,
None => return false,
};
if rt.header_len >= raw.len() {
return false;
}
let dot11_buf = &raw[rt.header_len..];
let frame = match dot11::parse(dot11_buf) {
Some(f) => f,
None => return false,
};
let now = SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0);
match frame.kind {
FrameKind::Beacon | FrameKind::ProbeResponse => {
if let Some(bssid) = frame.bssid_str() {
let info = frame.management_info();
let channel =
info.channel_ie.or_else(|| rt.freq_mhz.and_then(radiotap::freq_to_channel));
let entry = self.aps.entry(bssid.clone()).or_insert_with(|| AccessPoint {
bssid,
ssid: None,
channel: None,
security: None,
last_signal_dbm: None,
beacons_seen: 0,
clients: Vec::new(),
last_seen_unix: now,
});
if let Some(ssid) = info.ssid {
entry.ssid = Some(ssid);
}
if channel.is_some() {
entry.channel = channel;
}
entry.security = info.security.map(security_label);
entry.last_signal_dbm = rt.signal_dbm;
entry.beacons_seen += 1;
entry.last_seen_unix = now;
}
}
FrameKind::Data => {
// Associa o cliente (endereço de origem) ao BSSID observado,
// quando a direção do quadro indicar que vem de uma estação.
if let (Some(bssid), Some(src)) = (frame.bssid_str(), frame.src_str()) {
if let Some(ap) = self.aps.get_mut(&bssid) {
if src != bssid && !ap.clients.contains(&src) {
ap.clients.push(src);
}
}
}
return dot11::is_eapol(frame.body);
}
_ => {}
}
false
}
pub fn access_points(&self) -> Vec<AccessPoint> {
let mut v: Vec<_> = self.aps.values().cloned().collect();
v.sort_by(|a, b| b.beacons_seen.cmp(&a.beacons_seen));
v
}
}
fn security_label(s: Security) -> String {
match s {
Security::Open => "Aberta".into(),
Security::Wep => "WEP".into(),
Security::Wpa => "WPA".into(),
Security::Wpa2 => "WPA2".into(),
Security::Wpa3 => "WPA3".into(),
}
}