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:
parent
c2050f311a
commit
479c66d52c
18 changed files with 1205 additions and 38 deletions
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue