From 104767c04a1313fa46031bc231f763dd1f6bcc98 Mon Sep 17 00:00:00 2001 From: Lars Francke Date: Mon, 30 Mar 2026 10:45:28 +0200 Subject: [PATCH 01/11] fix: Log GID instead of UID in user GID tracing field The tracing statement for `user.gid` was reading from `user.uid` instead of `user.gid`, causing the wrong value to be reported. --- src/system_information/user.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/system_information/user.rs b/src/system_information/user.rs index 703cd09..f803be4 100644 --- a/src/system_information/user.rs +++ b/src/system_information/user.rs @@ -53,7 +53,7 @@ impl User { tracing::info!( user.name, user.uid = user.uid.as_ref().map(|uid| format!("{uid:?}")), - user.gid = user.uid.as_ref().map(|gid| format!("{gid:?}")), + user.gid = user.gid.as_ref().map(|gid| format!("{gid:?}")), "current user" ); Ok(user) From 8b5c1bb635f78363cd0f4ad5032a5d21f697abbc Mon Sep 17 00:00:00 2001 From: Lars Francke Date: Mon, 30 Mar 2026 10:48:08 +0200 Subject: [PATCH 02/11] refactor: Use idiomatic empty check for disk collection Replace `into_iter().next().is_none()` with `list().is_empty()` for clarity, and use `list().iter()` for the actual collection. --- src/system_information/disk.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/system_information/disk.rs b/src/system_information/disk.rs index a3ba514..ffe7395 100644 --- a/src/system_information/disk.rs +++ b/src/system_information/disk.rs @@ -12,10 +12,10 @@ impl Disk { #[tracing::instrument(name = "Disk::collect_all")] pub fn collect_all() -> Vec { let disks = sysinfo::Disks::new_with_refreshed_list(); - if disks.into_iter().next().is_none() { + if disks.list().is_empty() { tracing::info!("no disks found"); } - disks.into_iter().map(Self::from).collect() + disks.list().iter().map(Self::from).collect() } } From fa712775e8bcb9744865639c34d389940669207d Mon Sep 17 00:00:00 2001 From: Lars Francke Date: Mon, 30 Mar 2026 10:48:46 +0200 Subject: [PATCH 03/11] fix: Remove stale .source() call with unused result MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This was likely a debugging leftover — the error source chain is already captured via the `successors` iterator below. --- src/error.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/error.rs b/src/error.rs index f231a76..672323b 100644 --- a/src/error.rs +++ b/src/error.rs @@ -31,7 +31,6 @@ impl ComponentResult { error = &err as &dyn std::error::Error, "error reported by {component}, ignoring...", ); - err.source(); ComponentResult::Err { inner: ComponentError { message: err.to_string(), From 5dc84545a5de642bf3dfd0ce0afbcd72b6230bf1 Mon Sep 17 00:00:00 2001 From: Lars Francke Date: Mon, 30 Mar 2026 10:49:09 +0200 Subject: [PATCH 04/11] fix: Fix typo "proess" -> "process" in error message --- src/system_information/user.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/system_information/user.rs b/src/system_information/user.rs index f803be4..53759cd 100644 --- a/src/system_information/user.rs +++ b/src/system_information/user.rs @@ -8,7 +8,7 @@ use crate::error::SysinfoError; pub enum Error { #[snafu(display("failed to get pid of the current process"))] GetCurrentPid { source: SysinfoError }, - #[snafu(display("current pid {pid} could not be resolved to a proess"))] + #[snafu(display("current pid {pid} could not be resolved to a process"))] ResolveCurrentProcess { pid: Pid }, } type Result = std::result::Result; From de9b1b190c79058364c7a65573a43e59b68f44f5 Mon Sep 17 00:00:00 2001 From: Lars Francke Date: Mon, 30 Mar 2026 10:51:08 +0200 Subject: [PATCH 05/11] fix: Replace unwrap() calls with error logging in collection loop JSON serialization and file write can fail at runtime (e.g. disk full). Log the error and continue the loop instead of crashing, since this tool may run continuously for hours. --- src/main.rs | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/main.rs b/src/main.rs index 5e40325..41b8453 100644 --- a/src/main.rs +++ b/src/main.rs @@ -72,9 +72,24 @@ async fn main() { let system_information = SystemInformation::collect(&mut collect_ctx).await; - let serialized = serde_json::to_string_pretty(&system_information).unwrap(); - if let Some(output_path) = &opts.output { - std::fs::write(output_path, &serialized).unwrap(); + match serde_json::to_string_pretty(&system_information) { + Ok(serialized) => { + if let Some(output_path) = &opts.output + && let Err(err) = std::fs::write(output_path, &serialized) + { + tracing::error!( + path = %output_path.display(), + error = &err as &dyn std::error::Error, + "failed to write JSON output file" + ); + } + } + Err(err) => { + tracing::error!( + error = &err as &dyn std::error::Error, + "failed to serialize system information" + ); + } } match opts.loop_interval { From 4044279c2c3d7ed8a4d4263cfab7577774bf5299 Mon Sep 17 00:00:00 2001 From: Lars Francke Date: Mon, 30 Mar 2026 10:57:57 +0200 Subject: [PATCH 06/11] fix: Use tokio::time::sleep instead of std::thread::sleep std::thread::sleep blocks the entire tokio worker thread. Since main is already async, use the non-blocking alternative. --- src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index 41b8453..66f10e2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -68,7 +68,7 @@ async fn main() { if !next_run_sleep.is_zero() { tracing::info!(?next_run, "scheduling next run..."); } - std::thread::sleep(next_run_sleep); + tokio::time::sleep(next_run_sleep).await; let system_information = SystemInformation::collect(&mut collect_ctx).await; From aedcc1db49bffce2528bbce25a706fe5b4f66bc7 Mon Sep 17 00:00:00 2001 From: Lars Francke Date: Mon, 30 Mar 2026 10:59:54 +0200 Subject: [PATCH 07/11] fix: Handle DNS resolver initialization failure gracefully In a container debugging tool, broken DNS config (/etc/resolv.conf) is a likely scenario to diagnose. Log the error and skip DNS lookups instead of panicking. --- src/system_information/network.rs | 49 +++++++++++++++++++------------ 1 file changed, 31 insertions(+), 18 deletions(-) diff --git a/src/system_information/network.rs b/src/system_information/network.rs index e33171e..f0a5fce 100644 --- a/src/system_information/network.rs +++ b/src/system_information/network.rs @@ -11,14 +11,24 @@ use std::{ }; use tokio::task::JoinSet; -static GLOBAL_DNS_RESOLVER: LazyLock = LazyLock::new(|| { - let (resolver_config, mut resolver_opts) = - read_system_conf().expect("failed to read system resolv config"); +static GLOBAL_DNS_RESOLVER: LazyLock> = LazyLock::new(|| { + let (resolver_config, mut resolver_opts) = match read_system_conf() { + Ok(conf) => conf, + Err(err) => { + tracing::error!( + error = &err as &dyn std::error::Error, + "failed to read system DNS config, DNS lookups will be skipped" + ); + return None; + } + }; resolver_opts.timeout = Duration::from_secs(5); - TokioResolver::builder_with_config(resolver_config, TokioConnectionProvider::default()) - .with_options(resolver_opts) - .build() + Some( + TokioResolver::builder_with_config(resolver_config, TokioConnectionProvider::default()) + .with_options(resolver_opts) + .build(), + ) }); /// Captures all system network information, including network interfaces, @@ -64,12 +74,19 @@ impl SystemNetworkInfo { let ips: BTreeSet = interfaces.values().flatten().copied().collect(); tracing::info!(network.addresses.ip = ?ips, "ip addresses"); - let mut reverse_lookups = JoinSet::new(); + let Some(resolver) = GLOBAL_DNS_RESOLVER.as_ref() else { + return SystemNetworkInfo { + interfaces, + reverse_lookups: HashMap::new(), + forward_lookups: HashMap::new(), + }; + }; + + let mut reverse_lookup_tasks = JoinSet::new(); for ip in ips { - reverse_lookups - .spawn(async move { (ip, GLOBAL_DNS_RESOLVER.reverse_lookup(ip).await) }); + reverse_lookup_tasks.spawn(async move { (ip, resolver.reverse_lookup(ip).await) }); } - let reverse_lookups: HashMap> = reverse_lookups + let reverse_lookups: HashMap> = reverse_lookup_tasks .join_all() .await .into_iter() @@ -96,16 +113,12 @@ impl SystemNetworkInfo { let hostname_set: BTreeSet = reverse_lookups.values().flatten().cloned().collect(); tracing::info!(network.addresses.hostname = ?hostname_set, "hostnames"); - let mut forward_lookups = JoinSet::new(); + let mut forward_lookup_tasks = JoinSet::new(); for hostname in hostname_set { - forward_lookups.spawn(async move { - ( - hostname.clone(), - GLOBAL_DNS_RESOLVER.lookup_ip(hostname).await, - ) - }); + forward_lookup_tasks + .spawn(async move { (hostname.clone(), resolver.lookup_ip(hostname).await) }); } - let forward_lookups: HashMap> = forward_lookups + let forward_lookups: HashMap> = forward_lookup_tasks .join_all() .await .into_iter() From eb39937d5acd903828b5767a3d0e1184a81ba41a Mon Sep 17 00:00:00 2001 From: Lars Francke Date: Mon, 30 Mar 2026 11:02:58 +0200 Subject: [PATCH 08/11] fix: Wrap network collector in ComponentResult for consistent error handling The network collector silently swallowed interface listing errors by returning empty data. Now it returns Result so the orchestrator wraps it in ComponentResult, matching the pattern used by other fallible collectors. Errors appear in JSON output instead of being silently lost. --- src/system_information/mod.rs | 7 ++-- src/system_information/network.rs | 58 ++++++++++++++----------------- 2 files changed, 32 insertions(+), 33 deletions(-) diff --git a/src/system_information/mod.rs b/src/system_information/mod.rs index e826fcb..589408b 100644 --- a/src/system_information/mod.rs +++ b/src/system_information/mod.rs @@ -15,7 +15,7 @@ pub struct SystemInformation { pub os: Option, pub current_user: Option>, pub disks: Option>, - pub network: Option, + pub network: Option>, // TODO: // Current time // SElinux/AppArmor @@ -70,7 +70,10 @@ impl SystemInformation { user::User::collect_current(&ctx.system), )), disks: Some(disk::Disk::collect_all()), - network: Some(network::SystemNetworkInfo::collect().await), + network: Some(ComponentResult::report_from_result( + "SystemNetworkInfo::collect", + network::SystemNetworkInfo::collect().await, + )), // ..Default::default() }; diff --git a/src/system_information/network.rs b/src/system_information/network.rs index f0a5fce..4d3622e 100644 --- a/src/system_information/network.rs +++ b/src/system_information/network.rs @@ -3,6 +3,7 @@ use hickory_resolver::{ }; use local_ip_address::list_afinet_netifas; use serde::Serialize; +use snafu::{ResultExt, Snafu}; use std::{ collections::{BTreeSet, HashMap}, net::IpAddr, @@ -11,6 +12,12 @@ use std::{ }; use tokio::task::JoinSet; +#[derive(Debug, Snafu)] +pub enum Error { + #[snafu(display("failed to list network interfaces"))] + ListInterfaces { source: local_ip_address::Error }, +} + static GLOBAL_DNS_RESOLVER: LazyLock> = LazyLock::new(|| { let (resolver_config, mut resolver_opts) = match read_system_conf() { Ok(conf) => conf, @@ -42,44 +49,33 @@ pub struct SystemNetworkInfo { impl SystemNetworkInfo { #[tracing::instrument(name = "SystemNetworkInfo::collect")] - pub async fn collect() -> SystemNetworkInfo { - let interfaces = match list_afinet_netifas() { - Ok(netifs) => { - let mut interface_map = std::collections::HashMap::new(); + pub async fn collect() -> Result { + let netifs = list_afinet_netifas().context(ListInterfacesSnafu)?; + let mut interfaces = HashMap::new(); - // Iterate over the network interfaces and group them by name - // An interface may appear multiple times if it has multiple IP addresses (e.g. IPv4 and IPv6) - for (name, ip_addr) in netifs { - tracing::info!( - network.interface.name = name, - network.interface.address = %ip_addr, - "found network interface" - ); - interface_map - .entry(name) - .or_insert_with(Vec::new) - .push(ip_addr); - } - interface_map - } - Err(error) => { - tracing::error!( - error = &error as &dyn std::error::Error, - "failed to list network interfaces" - ); - HashMap::new() - } - }; + // Iterate over the network interfaces and group them by name + // An interface may appear multiple times if it has multiple IP addresses (e.g. IPv4 and IPv6) + for (name, ip_addr) in netifs { + tracing::info!( + network.interface.name = name, + network.interface.address = %ip_addr, + "found network interface" + ); + interfaces + .entry(name) + .or_insert_with(Vec::new) + .push(ip_addr); + } let ips: BTreeSet = interfaces.values().flatten().copied().collect(); tracing::info!(network.addresses.ip = ?ips, "ip addresses"); let Some(resolver) = GLOBAL_DNS_RESOLVER.as_ref() else { - return SystemNetworkInfo { + return Ok(SystemNetworkInfo { interfaces, reverse_lookups: HashMap::new(), forward_lookups: HashMap::new(), - }; + }); }; let mut reverse_lookup_tasks = JoinSet::new(); @@ -139,10 +135,10 @@ impl SystemNetworkInfo { }) .collect(); - SystemNetworkInfo { + Ok(SystemNetworkInfo { interfaces, reverse_lookups, forward_lookups, - } + }) } } From 465decad7d7dd02047c61b14134f981d35e5cff2 Mon Sep 17 00:00:00 2001 From: Lars Francke Date: Mon, 30 Mar 2026 11:04:28 +0200 Subject: [PATCH 09/11] refactor: Use BTreeMap for deterministic JSON key ordering HashMap produces non-deterministic JSON output, making it hard to diff containerdebug output across runs. BTreeMap sorts keys consistently. --- src/system_information/network.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/system_information/network.rs b/src/system_information/network.rs index 4d3622e..c73edae 100644 --- a/src/system_information/network.rs +++ b/src/system_information/network.rs @@ -5,7 +5,7 @@ use local_ip_address::list_afinet_netifas; use serde::Serialize; use snafu::{ResultExt, Snafu}; use std::{ - collections::{BTreeSet, HashMap}, + collections::{BTreeMap, BTreeSet}, net::IpAddr, sync::LazyLock, time::Duration, @@ -42,16 +42,16 @@ static GLOBAL_DNS_RESOLVER: LazyLock> = LazyLock::new(|| { /// and the results of reverse and forward DNS lookups. #[derive(Debug, Serialize)] pub struct SystemNetworkInfo { - pub interfaces: HashMap>, - pub reverse_lookups: HashMap>, - pub forward_lookups: HashMap>, + pub interfaces: BTreeMap>, + pub reverse_lookups: BTreeMap>, + pub forward_lookups: BTreeMap>, } impl SystemNetworkInfo { #[tracing::instrument(name = "SystemNetworkInfo::collect")] pub async fn collect() -> Result { let netifs = list_afinet_netifas().context(ListInterfacesSnafu)?; - let mut interfaces = HashMap::new(); + let mut interfaces = BTreeMap::new(); // Iterate over the network interfaces and group them by name // An interface may appear multiple times if it has multiple IP addresses (e.g. IPv4 and IPv6) @@ -73,8 +73,8 @@ impl SystemNetworkInfo { let Some(resolver) = GLOBAL_DNS_RESOLVER.as_ref() else { return Ok(SystemNetworkInfo { interfaces, - reverse_lookups: HashMap::new(), - forward_lookups: HashMap::new(), + reverse_lookups: BTreeMap::new(), + forward_lookups: BTreeMap::new(), }); }; @@ -82,7 +82,7 @@ impl SystemNetworkInfo { for ip in ips { reverse_lookup_tasks.spawn(async move { (ip, resolver.reverse_lookup(ip).await) }); } - let reverse_lookups: HashMap> = reverse_lookup_tasks + let reverse_lookups: BTreeMap> = reverse_lookup_tasks .join_all() .await .into_iter() @@ -114,7 +114,7 @@ impl SystemNetworkInfo { forward_lookup_tasks .spawn(async move { (hostname.clone(), resolver.lookup_ip(hostname).await) }); } - let forward_lookups: HashMap> = forward_lookup_tasks + let forward_lookups: BTreeMap> = forward_lookup_tasks .join_all() .await .into_iter() From f2e5f8e0d0af85e94ff36e555e6b2e5edd9becc9 Mon Sep 17 00:00:00 2001 From: Techassi Date: Mon, 30 Mar 2026 14:15:16 +0200 Subject: [PATCH 10/11] fix: Bump rustls-webpki to 0.103.10 to negate RUSTSEC-2026-0049 --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f54abc7..5c6160c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2316,9 +2316,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.9" +version = "0.103.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" dependencies = [ "ring", "rustls-pki-types", From 206a88c8fec702cf5c1fb6e73595062d1ef75945 Mon Sep 17 00:00:00 2001 From: Techassi Date: Mon, 30 Mar 2026 14:18:26 +0200 Subject: [PATCH 11/11] chore: Remove undetected, ignored advisories --- deny.toml | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/deny.toml b/deny.toml index 8ec7e45..54815a8 100644 --- a/deny.toml +++ b/deny.toml @@ -14,31 +14,6 @@ targets = [ [advisories] yanked = "deny" -ignore = [ - # https://rustsec.org/advisories/RUSTSEC-2023-0071 - # "rsa" crate: Marvin Attack: potential key recovery through timing sidechannel - # - # No patch is yet available, however work is underway to migrate to a fully constant-time implementation - # So we need to accept this, as of SDP 25.3 we are not using the rsa crate to create certificates used in production - # setups. - # - # https://github.com/RustCrypto/RSA/issues/19 is the tracking issue - "RUSTSEC-2023-0071", - - # https://rustsec.org/advisories/RUSTSEC-2024-0436 - # The "paste" crate is no longer maintained because the owner states that the implementation is - # finished. There are at least two (forked) alternatives which state to be maintained. They'd - # need to be vetted before a potential switch. Additionally, they'd need to be in a maintained - # state for a couple of years to provide any benefit over using "paste". - # - # This crate is only used in a single place in the xtask package inside the declarative - # "write_crd" macro. The impact of vulnerabilities, if any, should be fairly minimal. - # - # See thread: https://users.rust-lang.org/t/paste-alternatives/126787/4 - # - # This can only be removed again if we decide to use a different crate. - "RUSTSEC-2024-0436", -] [bans] multiple-versions = "allow"