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
125
apps/desktop/src-tauri/Cargo.lock
generated
125
apps/desktop/src-tauri/Cargo.lock
generated
|
|
@ -71,6 +71,7 @@ dependencies = [
|
|||
"sysinfo",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-keyring",
|
||||
"tauri-plugin-opener",
|
||||
"tauri-plugin-store",
|
||||
"thiserror 1.0.69",
|
||||
|
|
@ -544,6 +545,16 @@ dependencies = [
|
|||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.10.1"
|
||||
|
|
@ -567,7 +578,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"core-foundation",
|
||||
"core-foundation 0.10.1",
|
||||
"core-graphics-types",
|
||||
"foreign-types",
|
||||
"libc",
|
||||
|
|
@ -580,7 +591,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"core-foundation",
|
||||
"core-foundation 0.10.1",
|
||||
"libc",
|
||||
]
|
||||
|
||||
|
|
@ -718,6 +729,27 @@ dependencies = [
|
|||
"syn 2.0.106",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dbus"
|
||||
version = "0.9.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "190b6255e8ab55a7b568df5a883e9497edc3e4821c06396612048b430e5ad1e9"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"libdbus-sys",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dbus-secret-service"
|
||||
version = "4.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "708b509edf7889e53d7efb0ffadd994cc6c2345ccb62f55cfd6b0682165e4fa6"
|
||||
dependencies = [
|
||||
"dbus",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.5.4"
|
||||
|
|
@ -1955,6 +1987,21 @@ dependencies = [
|
|||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "keyring"
|
||||
version = "3.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eebcc3aff044e5944a8fbaf69eb277d11986064cba30c468730e8b9909fb551c"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"dbus-secret-service",
|
||||
"log",
|
||||
"security-framework 2.11.1",
|
||||
"security-framework 3.5.1",
|
||||
"windows-sys 0.60.2",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kuchikiki"
|
||||
version = "0.8.8-speedreader"
|
||||
|
|
@ -2003,6 +2050,15 @@ version = "0.2.176"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174"
|
||||
|
||||
[[package]]
|
||||
name = "libdbus-sys"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5cbe856efeb50e4681f010e9aaa2bf0a644e10139e54cde10fc83a307c23bd9f"
|
||||
dependencies = [
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libloading"
|
||||
version = "0.7.4"
|
||||
|
|
@ -3417,6 +3473,42 @@ version = "1.2.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "security-framework"
|
||||
version = "2.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"core-foundation 0.9.4",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
"security-framework-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework"
|
||||
version = "3.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"core-foundation 0.10.1",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
"security-framework-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework-sys"
|
||||
version = "2.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "selectors"
|
||||
version = "0.24.0"
|
||||
|
|
@ -3866,7 +3958,7 @@ checksum = "959469667dbcea91e5485fc48ba7dd6023face91bb0f1a14681a70f99847c3f7"
|
|||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"block2 0.6.2",
|
||||
"core-foundation",
|
||||
"core-foundation 0.10.1",
|
||||
"core-graphics",
|
||||
"crossbeam-channel",
|
||||
"dispatch",
|
||||
|
|
@ -4047,6 +4139,19 @@ dependencies = [
|
|||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-keyring"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a52455d6472f3c0f9cac1b1f017cfce85e177db32ccd5f4b50e2e31deeaf25e"
|
||||
dependencies = [
|
||||
"keyring",
|
||||
"serde",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"thiserror 2.0.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-opener"
|
||||
version = "2.5.0"
|
||||
|
|
@ -5691,6 +5796,20 @@ name = "zeroize"
|
|||
version = "1.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
|
||||
dependencies = [
|
||||
"zeroize_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize_derive"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerotrie"
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ tauri-build = { version = "2", features = [] }
|
|||
tauri = { version = "2", features = ["wry"] }
|
||||
tauri-plugin-opener = "2"
|
||||
tauri-plugin-store = "2.4"
|
||||
tauri-plugin-keyring = "0.1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
sysinfo = { version = "0.31", default-features = false, features = ["multithread", "network", "system", "disk"] }
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue