feat: melhorar inventário e gestão de máquinas

This commit is contained in:
Esdras Renan 2025-10-10 23:20:21 -03:00
parent b1d334045d
commit 3f0702d80b
5 changed files with 584 additions and 59 deletions

View file

@ -6,8 +6,8 @@ use once_cell::sync::Lazy;
use parking_lot::Mutex;
use serde::Serialize;
use serde_json::json;
use sysinfo::{Networks, System};
use std::collections::HashMap;
use sysinfo::{DiskExt, Networks, System, SystemExt};
use tauri::async_runtime::{self, JoinHandle};
use tokio::sync::Notify;
@ -166,27 +166,90 @@ fn collect_network_addrs() -> Vec<serde_json::Value> {
entries
}
fn collect_disks(_system: &System) -> Vec<serde_json::Value> {
// API de discos mudou no sysinfo 0.31: usamos Disks diretamente
fn collect_disks(system: &System) -> Vec<serde_json::Value> {
let mut out = Vec::new();
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 = d.file_system().to_string_lossy().to_string();
let total = d.total_space();
let avail = d.available_space();
for disk in system.disks() {
let name = disk.name().to_string_lossy().to_string();
let mount = disk.mount_point().to_string_lossy().to_string();
let fs = String::from_utf8_lossy(disk.file_system()).to_string();
let total = disk.total_space();
let avail = disk.available_space();
out.push(json!({
"name": name,
"name": if name.is_empty() { mount.clone() } else { name },
"mountPoint": mount,
"fs": fs,
"totalBytes": total,
"availableBytes": avail,
}));
}
if out.is_empty() {
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 total = d.total_space();
let avail = d.available_space();
out.push(json!({
"name": if name.is_empty() { mount.clone() } else { name },
"mountPoint": mount,
"fs": fs,
"totalBytes": total,
"availableBytes": avail,
}));
}
}
out
}
fn parse_u64(value: &serde_json::Value) -> Option<u64> {
if let Some(num) = value.as_u64() {
return Some(num);
}
if let Some(num) = value.as_f64() {
if num.is_finite() && num >= 0.0 {
return Some(num as u64);
}
}
if let Some(text) = value.as_str() {
if let Ok(parsed) = text.trim().parse::<u64>() {
return Some(parsed);
}
}
None
}
fn push_gpu(
list: &mut Vec<serde_json::Value>,
name: Option<&str>,
memory: Option<u64>,
vendor: Option<&str>,
driver: Option<&str>,
) {
if let Some(name) = name {
if name.trim().is_empty() {
return;
}
let mut obj = serde_json::Map::new();
obj.insert("name".into(), json!(name.trim()));
if let Some(memory) = memory {
obj.insert("memoryBytes".into(), json!(memory));
}
if let Some(vendor) = vendor {
if !vendor.trim().is_empty() {
obj.insert("vendor".into(), json!(vendor.trim()));
}
}
if let Some(driver) = driver {
if !driver.trim().is_empty() {
obj.insert("driver".into(), json!(driver.trim()));
}
}
list.push(serde_json::Value::Object(obj));
}
}
fn build_inventory_metadata(system: &System) -> serde_json::Value {
let cpu_brand = system
.cpus()
@ -204,6 +267,26 @@ fn build_inventory_metadata(system: &System) -> serde_json::Value {
"disks": disks,
});
if let Some(obj) = inventory.as_object_mut() {
let mut hardware = serde_json::Map::new();
if let Some(brand) = cpu_brand.clone() {
if !brand.trim().is_empty() {
hardware.insert("cpuType".into(), json!(brand.trim()));
}
}
if let Some(physical) = system.physical_core_count() {
hardware.insert("physicalCores".into(), json!(physical));
}
hardware.insert("logicalCores".into(), json!(system.cpus().len()));
if mem_total_bytes > 0 {
hardware.insert("memoryBytes".into(), json!(mem_total_bytes));
hardware.insert("memory".into(), json!(mem_total_bytes));
}
if !hardware.is_empty() {
obj.insert("hardware".into(), serde_json::Value::Object(hardware));
}
}
#[cfg(target_os = "linux")]
{
// Softwares instalados (dpkg ou rpm)
@ -253,10 +336,19 @@ fn build_inventory_metadata(system: &System) -> serde_json::Value {
if let Some(win) = ext.get("windows").and_then(|v| v.as_object()) {
if let Some(ws) = win.get("software").and_then(|v| v.as_array()) {
for item in ws {
let name = item.get("DisplayName").or_else(|| item.get("name")).cloned().unwrap_or(json!(null));
let version = item.get("DisplayVersion").or_else(|| item.get("version")).cloned().unwrap_or(json!(null));
let name = item
.get("DisplayName")
.or_else(|| item.get("name"))
.cloned()
.unwrap_or(json!(null));
let version = item
.get("DisplayVersion")
.or_else(|| item.get("version"))
.cloned()
.unwrap_or(json!(null));
let publisher = item.get("Publisher").cloned().unwrap_or(json!(null));
software.push(json!({ "name": name, "version": version, "source": publisher }));
software
.push(json!({ "name": name, "version": version, "source": publisher }));
}
}
}
@ -285,7 +377,9 @@ fn build_inventory_metadata(system: &System) -> serde_json::Value {
let name = s.get("Name").cloned().unwrap_or(json!(null));
let status = s.get("Status").cloned().unwrap_or(json!(null));
let display = s.get("DisplayName").cloned().unwrap_or(json!(null));
services.push(json!({ "name": name, "status": status, "displayName": display }));
services.push(
json!({ "name": name, "status": status, "displayName": display }),
);
}
}
}
@ -293,6 +387,106 @@ fn build_inventory_metadata(system: &System) -> serde_json::Value {
if !services.is_empty() {
obj.insert("services".into(), json!(services));
}
let mut gpus: Vec<serde_json::Value> = Vec::new();
if let Some(ext) = obj.get("extended").and_then(|v| v.as_object()) {
if let Some(win) = ext.get("windows").and_then(|v| v.as_object()) {
if let Some(video) = win.get("videoControllers").and_then(|v| v.as_array()) {
for controller in video {
let name = controller.get("Name").and_then(|v| v.as_str());
let memory = controller.get("AdapterRAM").and_then(parse_u64);
let driver = controller.get("DriverVersion").and_then(|v| v.as_str());
push_gpu(&mut gpus, name, memory, None, driver);
}
}
if obj
.get("disks")
.and_then(|v| v.as_array())
.map(|arr| arr.is_empty())
.unwrap_or(true)
{
if let Some(raw) = win.get("disks").and_then(|v| v.as_array()) {
let mapped = raw
.iter()
.map(|disk| {
let model = disk.get("Model").and_then(|v| v.as_str()).unwrap_or_default();
let serial = disk.get("SerialNumber").and_then(|v| v.as_str()).unwrap_or_default();
let size = parse_u64(disk.get("Size").unwrap_or(&serde_json::Value::Null)).unwrap_or(0);
let interface = disk.get("InterfaceType").and_then(|v| v.as_str()).unwrap_or("");
let media = disk.get("MediaType").and_then(|v| v.as_str()).unwrap_or("");
json!({
"name": if !model.is_empty() { model } else { serial },
"mountPoint": "",
"fs": if !media.is_empty() { media } else { "" },
"interface": if !interface.is_empty() { interface } else { serde_json::Value::Null },
"serial": if !serial.is_empty() { serial } else { serde_json::Value::Null },
"totalBytes": size,
"availableBytes": serde_json::Value::Null,
})
})
.collect::<Vec<_>>();
if !mapped.is_empty() {
obj.insert("disks".into(), json!(mapped));
}
}
}
}
if let Some(linux) = ext.get("linux").and_then(|v| v.as_object()) {
if let Some(pci) = linux.get("pciList").and_then(|v| v.as_array()) {
for entry in pci {
if let Some(text) = entry.get("text").and_then(|v| v.as_str()) {
let lower = text.to_lowercase();
if lower.contains(" vga ")
|| lower.contains(" 3d controller")
|| lower.contains("display controller")
{
push_gpu(&mut gpus, Some(text), None, None, None);
}
}
}
}
}
if let Some(macos) = ext.get("macos").and_then(|v| v.as_object()) {
if let Some(profiler) = macos.get("systemProfiler").and_then(|v| v.as_object()) {
if let Some(displays) = profiler
.get("SPDisplaysDataType")
.and_then(|v| v.as_array())
{
for display in displays {
if let Some(d) = display.as_object() {
let name = d.get("_name").and_then(|v| v.as_str());
let vram = d
.get("spdisplays_vram")
.and_then(|v| v.as_str())
.and_then(|s| {
let digits = s.split_whitespace().next().unwrap_or("");
digits.parse::<u64>().ok().map(|n| {
if s.to_lowercase().contains("gb") {
n * 1024 * 1024 * 1024
} else if s.to_lowercase().contains("mb") {
n * 1024 * 1024
} else {
n
}
})
});
push_gpu(&mut gpus, name, vram, None, None);
}
}
}
}
}
}
if !gpus.is_empty() {
let entry = obj.entry("hardware").or_insert_with(|| json!({}));
if let Some(hardware) = entry.as_object_mut() {
hardware.insert("gpus".into(), json!(gpus.clone()));
if let Some(primary) = gpus.first() {
hardware.insert("primaryGpu".into(), primary.clone());
}
}
}
}
json!({ "inventory": inventory })
@ -369,7 +563,9 @@ fn collect_services_linux() -> serde_json::Value {
// Typical format: UNIT LOAD ACTIVE SUB DESCRIPTION
// We take UNIT and ACTIVE
let cols: Vec<&str> = line.split_whitespace().collect();
if cols.is_empty() { continue; }
if cols.is_empty() {
continue;
}
let unit = cols.get(0).unwrap_or(&"");
let active = cols.get(2).copied().unwrap_or("");
if !unit.is_empty() {
@ -391,7 +587,13 @@ fn collect_linux_extended() -> serde_json::Value {
.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(|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!({}));
@ -427,12 +629,10 @@ fn collect_linux_extended() -> serde_json::Value {
if let Ok(out) = std::process::Command::new("sh")
.arg("-lc")
.arg("command -v smartctl >/dev/null 2>&1 && echo yes || echo no")
.output() {
.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())
{
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("");
@ -441,9 +641,12 @@ fn collect_linux_extended() -> serde_json::Value {
if let Ok(out) = Command::new("sh")
.arg("-lc")
.arg(format!("smartctl -H -j {} 2>/dev/null || true", path))
.output() {
.output()
{
if out.status.success() || !out.stdout.is_empty() {
if let Ok(val) = serde_json::from_slice::<serde_json::Value>(&out.stdout) {
if let Ok(val) =
serde_json::from_slice::<serde_json::Value>(&out.stdout)
{
smart.push(val);
}
}
@ -468,8 +671,8 @@ fn collect_linux_extended() -> serde_json::Value {
#[cfg(target_os = "windows")]
fn collect_windows_extended() -> serde_json::Value {
use std::process::Command;
use std::os::windows::process::CommandExt;
use std::process::Command;
const CREATE_NO_WINDOW: u32 = 0x08000000;
fn ps(cmd: &str) -> Option<serde_json::Value> {
let ps_cmd = format!(
@ -479,19 +682,23 @@ fn collect_windows_extended() -> serde_json::Value {
let out = Command::new("powershell")
.creation_flags(CREATE_NO_WINDOW)
.arg("-NoProfile")
.arg("-WindowStyle").arg("Hidden")
.arg("-WindowStyle")
.arg("Hidden")
.arg("-NoLogo")
.arg("-Command")
.arg(ps_cmd)
.output()
.ok()?;
if out.stdout.is_empty() { return None; }
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 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!([]));
@ -514,7 +721,10 @@ fn collect_windows_extended() -> serde_json::Value {
// Hardware detalhado (CPU/Board/BIOS/Memória/Vídeo/Discos)
let cpu = ps("Get-CimInstance Win32_Processor | Select-Object Name,Manufacturer,SocketDesignation,NumberOfCores,NumberOfLogicalProcessors,L2CacheSize,L3CacheSize,MaxClockSpeed").unwrap_or_else(|| json!({}));
let baseboard = ps("Get-CimInstance Win32_BaseBoard | Select-Object Product,Manufacturer,SerialNumber,Version").unwrap_or_else(|| json!({}));
let baseboard = ps(
"Get-CimInstance Win32_BaseBoard | Select-Object Product,Manufacturer,SerialNumber,Version",
)
.unwrap_or_else(|| json!({}));
let bios = ps("Get-CimInstance Win32_BIOS | Select-Object Manufacturer,SMBIOSBIOSVersion,ReleaseDate,Version").unwrap_or_else(|| json!({}));
let memory = ps("@(Get-CimInstance Win32_PhysicalMemory | Select-Object BankLabel,Capacity,Manufacturer,PartNumber,SerialNumber,ConfiguredClockSpeed,Speed,ConfiguredVoltage)").unwrap_or_else(|| json!([]));
let video = ps("@(Get-CimInstance Win32_VideoController | Select-Object Name,AdapterRAM,DriverVersion,PNPDeviceID)").unwrap_or_else(|| json!([]));
@ -543,7 +753,7 @@ fn collect_macos_extended() -> serde_json::Value {
// 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")
.arg("system_profiler -json SPHardwareDataType SPSoftwareDataType SPNetworkDataType SPStorageDataType SPDisplaysDataType 2>/dev/null || true")
.output()
.ok()
.and_then(|out| serde_json::from_slice::<serde_json::Value>(&out.stdout).ok())
@ -553,7 +763,13 @@ fn collect_macos_extended() -> serde_json::Value {
.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<_>>())
.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")
@ -668,7 +884,11 @@ static HTTP_CLIENT: Lazy<reqwest::Client> = Lazy::new(|| {
.expect("failed to build http client")
});
async fn post_heartbeat(base_url: &str, token: &str, status: Option<String>) -> Result<(), AgentError> {
async fn post_heartbeat(
base_url: &str,
token: &str,
status: Option<String>,
) -> Result<(), AgentError> {
let system = collect_system();
let metrics = collect_metrics(&system);
let hostname = hostname::get()
@ -760,7 +980,9 @@ impl AgentRuntime {
let status_clone = status.clone();
let join_handle = async_runtime::spawn(async move {
if let Err(error) = post_heartbeat(&base_clone, &token_clone, status_clone.clone()).await {
if let Err(error) =
post_heartbeat(&base_clone, &token_clone, status_clone.clone()).await
{
eprintln!("[agent] Falha inicial ao enviar heartbeat: {error}");
}
@ -776,7 +998,9 @@ impl AgentRuntime {
_ = ticker.tick() => {}
}
if let Err(error) = post_heartbeat(&base_clone, &token_clone, status_clone.clone()).await {
if let Err(error) =
post_heartbeat(&base_clone, &token_clone, status_clone.clone()).await
{
eprintln!("[agent] Falha ao enviar heartbeat: {error}");
}
}