App desktop (Tauri), GitHub Actions de CI/release, elevação automática

- Move a UI de crates/sabia-cli/src/web/ para apps/gui/ui/: fonte única
  servida tanto pelo `sabia ui` quanto pelo app desktop.
- sabia-core::engine::Engine centraliza sessões de scan/monitor/update —
  usado pelo servidor local da CLI e pelo app Tauri, sem duplicar lógica.
- apps/gui/src-tauri: app desktop Tauri 2, mesma interface, comandos IPC
  espelhando a API HTTP. Pede elevação de administrador/root
  automaticamente ao abrir (modo monitor e captura crua exigem):
  manifesto requireAdministrator no Windows (UAC antes mesmo do app
  abrir), pkexec no Linux e osascript no macOS, com reinício automático
  e aviso + botão de nova tentativa na interface se for negada.
- Ícone gerado a partir do logo (apps/gui/src-tauri/icons/source.png).
- .github/workflows/ci.yml: fmt, clippy, testes (Linux/Windows/macOS),
  auditoria de dependências, compilação do app desktop.
- .github/workflows/release.yml: ao empurrar uma tag vX.Y.Z, builda a
  CLI (Linux x86_64/aarch64, macOS x86_64/aarch64, Windows x86_64) e o
  app desktop pras mesmas plataformas, publica tudo numa release do
  GitHub com checksums SHA-256.
- sabia-core::update: corrige o nome do asset esperado (casava pelo
  nome fixo, mas o release empacota com a versão no nome) e a extração
  (tar.gz no Linux/macOS, zip no Windows).
- LICENSE-MIT, LICENSE-APACHE, CHANGELOG.md com a entrada 0.1.0.
- cargo fmt + clippy (-D warnings) limpos em todo o workspace.
This commit is contained in:
Lucas committed 2026-10-23 08:56:08 -03:00
1 parent fd3ef5240e
commit df27974359
29 files changed
+1268 -191

No files matched your search

+139
View File
@@ -0,0 +1,139 @@
//! Ponte única entre o núcleo e as duas interfaces (servidor local da CLI e
//! o app desktop Tauri): ambos chamam exatamente os mesmos métodos, então
//! qualquer coisa que funcione em um funciona idêntico no outro.
use crate::audit::AuditLog;
use crate::error::Result;
use crate::iface::{self, Interface};
use crate::netlink;
use crate::scan::{AccessPoint, ScanTable};
use crate::update::{self, UpdateStatus};
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
struct Session {
table: Arc<Mutex<ScanTable>>,
running: Arc<AtomicBool>,
}
pub struct Engine {
sessions: Mutex<HashMap<String, Session>>,
audit_path: PathBuf,
}
impl Default for Engine {
fn default() -> Self {
Self::new()
}
}
impl Engine {
pub fn new() -> Self {
Self {
sessions: Mutex::new(HashMap::new()),
audit_path: PathBuf::from(".sabia-audit.log"),
}
}
pub fn version(&self) -> &'static str {
env!("CARGO_PKG_VERSION")
}
pub fn list_ifaces(&self) -> Result<Vec<Interface>> {
iface::list_wireless()
}
pub fn set_monitor(&self, name: &str, enable: bool) -> Result<()> {
netlink::set_monitor_mode(name, enable)?;
if let Ok(mut log) = AuditLog::open(&self.audit_path) {
let _ = log.record("monitor_mode", name, &format!("{{\"enable\":{enable}}}"));
}
Ok(())
}
#[cfg(target_os = "linux")]
pub fn start_scan(&self, name: &str) -> Result<()> {
use crate::capture::RawCapture;
use std::thread;
use std::time::Duration;
let mut sessions = self.sessions.lock().unwrap();
if sessions.contains_key(name) {
return Ok(());
}
let table = Arc::new(Mutex::new(ScanTable::new()));
let running = Arc::new(AtomicBool::new(true));
let t = table.clone();
let r = running.clone();
let iface_name = name.to_string();
thread::spawn(move || {
let cap = match RawCapture::open(&iface_name) {
Ok(c) => c,
Err(e) => {
eprintln!("falha ao abrir captura em {iface_name}: {e}");
return;
}
};
let _ = cap.set_read_timeout(Duration::from_millis(500));
let channels = netlink::supported_channels(&iface_name).unwrap_or_default();
let mut idx = 0usize;
let mut last_hop = std::time::Instant::now();
let mut buf = [0u8; 65535];
while r.load(Ordering::SeqCst) {
if !channels.is_empty() && last_hop.elapsed() > Duration::from_millis(300) {
idx = (idx + 1) % channels.len();
let _ = netlink::set_channel(&iface_name, channels[idx]);
last_hop = std::time::Instant::now();
}
if let Ok(Some(n)) = cap.recv(&mut buf) {
let mut table = t.lock().unwrap();
table.ingest(&buf[..n]);
}
}
});
if let Ok(mut log) = AuditLog::open(&self.audit_path) {
let _ = log.record("scan_start", name, "{}");
}
sessions.insert(name.to_string(), Session { table, running });
Ok(())
}
/// Captura crua (AF_PACKET) só existe no Linux (ver `crate::capture`).
/// Em macOS/Windows o app compila e roda, mas a captura ainda não tem
/// backend nativo — próximo passo natural do projeto.
#[cfg(not(target_os = "linux"))]
pub fn start_scan(&self, _name: &str) -> Result<()> {
Err(crate::error::SabiaError::UnsupportedPlatform)
}
pub fn stop_scan(&self, name: &str) {
let mut sessions = self.sessions.lock().unwrap();
if let Some(s) = sessions.remove(name) {
s.running.store(false, Ordering::SeqCst);
if let Ok(mut log) = AuditLog::open(&self.audit_path) {
let _ = log.record("scan_stop", name, "{}");
}
}
}
pub fn scan_results(&self, name: &str) -> Vec<AccessPoint> {
let sessions = self.sessions.lock().unwrap();
match sessions.get(name) {
Some(s) => s.table.lock().unwrap().access_points(),
None => Vec::new(),
}
}
pub fn update_check(&self) -> Result<UpdateStatus> {
update::check(self.version())
}
pub fn update_apply(&self) -> Result<String> {
let exe = std::env::current_exe()?;
update::apply(&exe)
}
}