feat(desktop-agent,admin/inventory): secure token storage via keyring; extended inventory collectors per OS; new /api/machines/inventory endpoint; posture rules + tickets; Admin UI inventory with filters, search and export; docs + CI desktop release

This commit is contained in:
Esdras Renan 2025-10-09 22:08:20 -03:00
parent c2050f311a
commit 479c66d52c
18 changed files with 1205 additions and 38 deletions

View file

@ -7,6 +7,7 @@ use parking_lot::Mutex;
use serde::Serialize;
use serde_json::json;
use sysinfo::{Networks, System};
use std::collections::HashMap;
use tauri::async_runtime::{self, JoinHandle};
use tokio::sync::Notify;
@ -131,13 +132,30 @@ fn collect_serials() -> Vec<String> {
}
fn collect_network_addrs() -> Vec<serde_json::Value> {
// Mapa name -> mac via sysinfo (mais estável que get_if_addrs para MAC)
let mut mac_by_name: HashMap<String, String> = HashMap::new();
let mut networks = Networks::new();
networks.refresh_list();
networks.refresh();
for (name, data) in networks.iter() {
let bytes = data.mac_address().0;
if bytes.iter().any(|b| *b != 0) {
let mac = bytes
.iter()
.map(|b| format!("{:02x}", b))
.collect::<Vec<_>>()
.join(":");
mac_by_name.insert(name.to_string(), mac);
}
}
let mut entries = Vec::new();
if let Ok(ifaces) = get_if_addrs::get_if_addrs() {
for iface in ifaces {
let name = iface.name;
let mac = iface.mac.map(|m| m.to_string());
let name = iface.name.clone();
let addr = iface.ip();
let ip = addr.to_string();
let mac = mac_by_name.get(&name).cloned();
entries.push(json!({
"name": name,
"mac": mac,
@ -148,12 +166,14 @@ fn collect_network_addrs() -> Vec<serde_json::Value> {
entries
}
fn collect_disks(system: &System) -> Vec<serde_json::Value> {
fn collect_disks(_system: &System) -> Vec<serde_json::Value> {
// API de discos mudou no sysinfo 0.31: usamos Disks diretamente
let mut out = Vec::new();
for d in system.disks() {
let disks = sysinfo::Disks::new_with_refreshed_list();
for d in disks.list() {
let name = d.name().to_string_lossy().to_string();
let mount = d.mount_point().to_string_lossy().to_string();
let fs = String::from_utf8_lossy(d.file_system()).to_string();
let fs = d.file_system().to_string_lossy().to_string();
let total = d.total_space();
let avail = d.available_space();
out.push(json!({
@ -196,6 +216,28 @@ fn build_inventory_metadata(system: &System) -> serde_json::Value {
if let Some(obj) = inventory.as_object_mut() {
obj.insert("services".into(), services);
}
// Informações estendidas (lsblk/lspci/lsusb/smartctl)
let extended = collect_linux_extended();
if let Some(obj) = inventory.as_object_mut() {
obj.insert("extended".into(), extended);
}
}
#[cfg(target_os = "windows")]
{
let extended = collect_windows_extended();
if let Some(obj) = inventory.as_object_mut() {
obj.insert("extended".into(), extended);
}
}
#[cfg(target_os = "macos")]
{
let extended = collect_macos_extended();
if let Some(obj) = inventory.as_object_mut() {
obj.insert("extended".into(), extended);
}
}
json!({ "inventory": inventory })
@ -276,6 +318,146 @@ fn collect_services_linux() -> serde_json::Value {
json!([])
}
#[cfg(target_os = "linux")]
fn collect_linux_extended() -> serde_json::Value {
use std::process::Command;
// lsblk em JSON (block devices)
let block_json = Command::new("sh")
.arg("-lc")
.arg("lsblk -J -b 2>/dev/null || true")
.output()
.ok()
.and_then(|out| if out.status.success() { Some(out.stdout) } else { Some(out.stdout) })
.and_then(|bytes| serde_json::from_slice::<serde_json::Value>(&bytes).ok())
.unwrap_or_else(|| json!({}));
// lspci e lsusb — texto livre (depende de pacotes pciutils/usbutils)
let lspci = Command::new("sh")
.arg("-lc")
.arg("lspci 2>/dev/null || true")
.output()
.ok()
.map(|out| String::from_utf8_lossy(&out.stdout).to_string())
.unwrap_or_default();
let lsusb = Command::new("sh")
.arg("-lc")
.arg("lsusb 2>/dev/null || true")
.output()
.ok()
.map(|out| String::from_utf8_lossy(&out.stdout).to_string())
.unwrap_or_default();
// smartctl (se disponível) por disco
let mut smart: Vec<serde_json::Value> = Vec::new();
if let Ok(out) = std::process::Command::new("sh")
.arg("-lc")
.arg("command -v smartctl >/dev/null 2>&1 && echo yes || echo no")
.output() {
if out.status.success() && String::from_utf8_lossy(&out.stdout).contains("yes") {
if let Some(devices) = block_json
.get("blockdevices")
.and_then(|v| v.as_array())
{
for dev in devices {
let t = dev.get("type").and_then(|v| v.as_str()).unwrap_or("");
let name = dev.get("name").and_then(|v| v.as_str()).unwrap_or("");
if t == "disk" && !name.is_empty() {
let path = format!("/dev/{}", name);
if let Ok(out) = Command::new("sh")
.arg("-lc")
.arg(format!("smartctl -H -j {} 2>/dev/null || true", path))
.output() {
if out.status.success() || !out.stdout.is_empty() {
if let Ok(val) = serde_json::from_slice::<serde_json::Value>(&out.stdout) {
smart.push(val);
}
}
}
}
}
}
}
}
json!({
"linux": {
"lsblk": block_json,
"lspci": lspci,
"lsusb": lsusb,
"smart": smart,
}
})
}
#[cfg(target_os = "windows")]
fn collect_windows_extended() -> serde_json::Value {
use std::process::Command;
fn ps(cmd: &str) -> Option<serde_json::Value> {
let ps_cmd = format!(
"$ErrorActionPreference='SilentlyContinue'; {} | ConvertTo-Json -Depth 4 -Compress",
cmd
);
let out = Command::new("powershell")
.arg("-NoProfile")
.arg("-Command")
.arg(ps_cmd)
.output()
.ok()?;
if out.stdout.is_empty() { return None; }
serde_json::from_slice::<serde_json::Value>(&out.stdout).ok()
}
let software = ps(r#"@(Get-ItemProperty 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*'; Get-ItemProperty 'HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*') | Where-Object { $_.DisplayName } | Select-Object DisplayName, DisplayVersion, Publisher"#)
.unwrap_or_else(|| json!([]));
let services = ps("Get-Service | Select-Object Name,Status,DisplayName").unwrap_or_else(|| json!([]));
let defender = ps("Get-MpComputerStatus | Select-Object AMRunningMode,AntivirusEnabled,RealTimeProtectionEnabled,AntispywareEnabled").unwrap_or_else(|| json!({}));
let hotfix = ps("Get-HotFix | Select-Object HotFixID,InstalledOn").unwrap_or_else(|| json!([]));
json!({
"windows": {
"software": software,
"services": services,
"defender": defender,
"hotfix": hotfix,
}
})
}
#[cfg(target_os = "macos")]
fn collect_macos_extended() -> serde_json::Value {
use std::process::Command;
// system_profiler em JSON (pode ser pesado; limitar a alguns tipos)
let profiler = Command::new("sh")
.arg("-lc")
.arg("system_profiler -json SPHardwareDataType SPSoftwareDataType SPNetworkDataType SPStorageDataType 2>/dev/null || true")
.output()
.ok()
.and_then(|out| serde_json::from_slice::<serde_json::Value>(&out.stdout).ok())
.unwrap_or_else(|| json!({}));
let pkgs = Command::new("sh")
.arg("-lc")
.arg("pkgutil --pkgs 2>/dev/null || true")
.output()
.ok()
.map(|out| String::from_utf8_lossy(&out.stdout).lines().map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect::<Vec<_>>())
.unwrap_or_default();
let services_text = Command::new("sh")
.arg("-lc")
.arg("launchctl list 2>/dev/null || true")
.output()
.ok()
.map(|out| String::from_utf8_lossy(&out.stdout).to_string())
.unwrap_or_default();
json!({
"macos": {
"systemProfiler": profiler,
"packages": pkgs,
"launchctl": services_text,
}
})
}
fn collect_system() -> System {
let mut system = System::new_all();
system.refresh_all();
@ -403,7 +585,6 @@ async fn post_heartbeat(base_url: &str, token: &str, status: Option<String>) ->
struct HeartbeatHandle {
token: String,
base_url: String,
status: Option<String>,
stop_signal: Arc<Notify>,
join_handle: JoinHandle<()>,
}
@ -489,7 +670,6 @@ impl AgentRuntime {
let handle = HeartbeatHandle {
token,
base_url: sanitized_base,
status,
stop_signal,
join_handle,
};

View file

@ -2,6 +2,7 @@ mod agent;
use agent::{collect_profile, AgentRuntime, MachineProfile};
use tauri_plugin_store::Builder as StorePluginBuilder;
use tauri_plugin_keyring as keyring;
#[tauri::command]
fn collect_machine_profile() -> Result<MachineProfile, String> {
@ -33,6 +34,7 @@ pub fn run() {
.manage(AgentRuntime::new())
.plugin(tauri_plugin_opener::init())
.plugin(StorePluginBuilder::default().build())
.plugin(keyring::init())
.invoke_handler(tauri::generate_handler![
collect_machine_profile,
start_machine_agent,