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:
1 parent
fd3ef5240e
commit
df27974359
29 files changed
+1268
-191
No files matched your search
@@ -0,0 +1,28 @@
|
||||
# App desktop (Tauri 2). Fica fora do workspace principal porque exige
|
||||
# as bibliotecas de WebView do sistema (WebKitGTK no Linux).
|
||||
[package]
|
||||
name = "sabia-gui"
|
||||
description = "Sabiá: interface gráfica de auditoria de Wi-Fi"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/lucasrguerra/sabia"
|
||||
rust-version = "1.80"
|
||||
|
||||
[workspace]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2", features = [] }
|
||||
serde_json = "1"
|
||||
sabia-core = { path = "../../../crates/sabia-core" }
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
libc = "0.2"
|
||||
|
||||
[profile.release]
|
||||
lto = "thin"
|
||||
codegen-units = 1
|
||||
strip = true
|
||||
@@ -0,0 +1,13 @@
|
||||
fn main() {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
// Modo monitor e captura crua exigem administrador no Windows: o
|
||||
// manifesto pede elevação (UAC) assim que o app abre.
|
||||
let manifest = include_str!("windows-app-manifest.xml");
|
||||
let attrs = tauri_build::Attributes::new()
|
||||
.windows_attributes(tauri_build::WindowsAttributes::new().app_manifest(manifest));
|
||||
tauri_build::try_build(attrs).expect("falha no tauri-build");
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
tauri_build::build();
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 92 KiB |
@@ -0,0 +1,190 @@
|
||||
// Evita a janela de console extra no Windows em release.
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
use sabia_core::engine::Engine;
|
||||
use std::sync::Arc;
|
||||
|
||||
struct State(Arc<Engine>);
|
||||
|
||||
#[tauri::command]
|
||||
fn version(state: tauri::State<'_, State>) -> String {
|
||||
state.0.version().to_string()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn list_ifaces(state: tauri::State<'_, State>) -> Result<serde_json::Value, String> {
|
||||
let list = state.0.list_ifaces().map_err(|e| e.to_string())?;
|
||||
serde_json::to_value(list).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn set_monitor(state: tauri::State<'_, State>, iface: String, enable: bool) -> Result<(), String> {
|
||||
state.0.set_monitor(&iface, enable).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn start_scan(state: tauri::State<'_, State>, iface: String) -> Result<(), String> {
|
||||
state.0.start_scan(&iface).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn stop_scan(state: tauri::State<'_, State>, iface: String) {
|
||||
state.0.stop_scan(&iface);
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn scan_results(state: tauri::State<'_, State>, iface: String) -> Result<serde_json::Value, String> {
|
||||
serde_json::to_value(state.0.scan_results(&iface)).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn update_check(state: tauri::State<'_, State>) -> Result<serde_json::Value, String> {
|
||||
let status = state.0.update_check().map_err(|e| e.to_string())?;
|
||||
serde_json::to_value(status).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn update_apply(state: tauri::State<'_, State>) -> Result<String, String> {
|
||||
state.0.update_apply().map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// `true` quando o processo já tem privilégio suficiente pra tudo que o
|
||||
/// Sabiá faz (modo monitor, captura crua). No Windows o manifesto já
|
||||
/// exige administrador antes mesmo do app abrir, então aqui é sempre `true`.
|
||||
#[tauri::command]
|
||||
fn is_elevated() -> bool {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
unsafe { libc::geteuid() == 0 }
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/// Reabre o app com privilégios de administrador (Linux/macOS). O app novo
|
||||
/// encerra este quando abre; se a autorização falhar ou for cancelada, a
|
||||
/// janela continua aqui e a interface mostra o motivo.
|
||||
#[tauri::command]
|
||||
fn relaunch_elevated(app: tauri::AppHandle) -> Result<(), String> {
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
{
|
||||
do_relaunch_elevated(app)
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let _ = app;
|
||||
Err("o Sabiá já roda como administrador no Windows".into())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
fn do_relaunch_elevated(app: tauri::AppHandle) -> Result<(), String> {
|
||||
use tauri::Emitter;
|
||||
let exe = elevated_exe()?;
|
||||
let log = std::env::temp_dir().join(format!("sabia-elevate-{}.log", std::process::id()));
|
||||
let _ = std::fs::write(&log, b"");
|
||||
let out = std::fs::OpenOptions::new().append(true).open(&log).map_err(|e| e.to_string())?;
|
||||
let err = out.try_clone().map_err(|e| e.to_string())?;
|
||||
let pid = std::process::id().to_string();
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
let mut cmd = {
|
||||
let mut c = std::process::Command::new("pkexec");
|
||||
c.arg("/usr/bin/env");
|
||||
for k in ["DISPLAY", "WAYLAND_DISPLAY", "XAUTHORITY", "XDG_RUNTIME_DIR", "DBUS_SESSION_BUS_ADDRESS", "GDK_BACKEND"] {
|
||||
if let Ok(v) = std::env::var(k) {
|
||||
c.arg(format!("{k}={v}"));
|
||||
}
|
||||
}
|
||||
c.arg(format!("SABIA_REPLACE_PID={pid}"));
|
||||
c.arg(&exe);
|
||||
c
|
||||
};
|
||||
#[cfg(target_os = "macos")]
|
||||
let mut cmd = {
|
||||
let shell = format!("SABIA_REPLACE_PID={} '{}' >/dev/null 2>&1 &", pid, exe.display());
|
||||
let script = format!("do shell script \"{}\" with administrator privileges", shell.replace('\\', "\\\\").replace('"', "\\\""));
|
||||
let mut c = std::process::Command::new("osascript");
|
||||
c.args(["-e", &script]);
|
||||
c
|
||||
};
|
||||
cmd.stdout(out).stderr(err);
|
||||
let mut child = cmd.spawn().map_err(|e| format!("não foi possível pedir privilégios: {e}"))?;
|
||||
std::thread::spawn(move || {
|
||||
let status = child.wait();
|
||||
if matches!(&status, Ok(s) if s.success()) {
|
||||
let _ = std::fs::remove_file(&log);
|
||||
return;
|
||||
}
|
||||
let detail = std::fs::read_to_string(&log).unwrap_or_default();
|
||||
let msg = match status.ok().and_then(|s| s.code()) {
|
||||
Some(126) => "Autorização cancelada.".to_string(),
|
||||
Some(127) => "Não foi possível autenticar como administrador.".to_string(),
|
||||
_ => format!("Não foi possível reiniciar como administrador. {}", detail.lines().last().unwrap_or("").trim()),
|
||||
};
|
||||
let _ = app.emit("elevate-failed", msg);
|
||||
let _ = std::fs::remove_file(&log);
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Caminho que o root consegue executar. No AppImage, `current_exe` aponta
|
||||
/// pra dentro de um ponto de montagem FUSE do usuário, inacessível ao root:
|
||||
/// usa o próprio arquivo `.AppImage` (variável `APPIMAGE`).
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
fn elevated_exe() -> Result<std::path::PathBuf, String> {
|
||||
if let Some(p) = std::env::var_os("APPIMAGE").map(std::path::PathBuf::from).filter(|p| p.is_file()) {
|
||||
return Ok(p);
|
||||
}
|
||||
std::env::current_exe().map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let engine = Arc::new(Engine::new());
|
||||
tauri::Builder::default()
|
||||
.manage(State(engine))
|
||||
.setup(|app| {
|
||||
// Quando abrimos elevados a pedido do app normal, ele fecha o app antigo.
|
||||
#[cfg(unix)]
|
||||
if let Some(pid) = std::env::var("SABIA_REPLACE_PID").ok().and_then(|p| p.parse::<i32>().ok()) {
|
||||
std::thread::spawn(move || {
|
||||
std::thread::sleep(std::time::Duration::from_millis(1200));
|
||||
// SAFETY: só envia SIGTERM ao processo anterior do próprio usuário.
|
||||
unsafe {
|
||||
if libc::geteuid() == 0 {
|
||||
libc::kill(pid, libc::SIGTERM);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Captura crua e modo monitor exigem root: pede elevação assim
|
||||
// que o app abre, sem esperar o usuário procurar um botão. Se
|
||||
// a autorização falhar, a janela continua aberta e a interface
|
||||
// mostra o aviso (com um botão pra tentar de novo).
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
if unsafe { libc::geteuid() != 0 } && std::env::var("SABIA_REPLACE_PID").is_err() {
|
||||
let handle = app.handle().clone();
|
||||
std::thread::spawn(move || {
|
||||
let _ = do_relaunch_elevated(handle);
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
version,
|
||||
list_ifaces,
|
||||
set_monitor,
|
||||
start_scan,
|
||||
stop_scan,
|
||||
scan_results,
|
||||
update_check,
|
||||
update_apply,
|
||||
is_elevated,
|
||||
relaunch_elevated,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("erro ao iniciar o Sabiá");
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Sabiá",
|
||||
"version": "0.1.0",
|
||||
"identifier": "dev.sabia.app",
|
||||
"build": {
|
||||
"frontendDist": "../ui"
|
||||
},
|
||||
"app": {
|
||||
"withGlobalTauri": true,
|
||||
"windows": [
|
||||
{
|
||||
"title": "Sabiá",
|
||||
"width": 1180,
|
||||
"height": 780,
|
||||
"minWidth": 420,
|
||||
"minHeight": 560,
|
||||
"resizable": true
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": "default-src 'self'; img-src 'self' data:; connect-src 'self' ipc: http://ipc.localhost; style-src 'self' 'unsafe-inline'; script-src 'self'; worker-src 'self'; frame-src 'none'; object-src 'none'"
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"category": "Utility",
|
||||
"shortDescription": "Auditoria de Wi-Fi",
|
||||
"longDescription": "Sabiá audita, monitora e testa a segurança de redes Wi-Fi — modo monitor, captura de quadros e handshakes, sempre com trilha de auditoria.",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||
<dependency>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*" />
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<security>
|
||||
<requestedPrivileges>
|
||||
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
</assembly>
|
||||
Reference in new issue
Block a user