diff --git a/backend/crates/api/src/config.rs b/backend/crates/api/src/config.rs index 87a06ae..b1f523b 100644 --- a/backend/crates/api/src/config.rs +++ b/backend/crates/api/src/config.rs @@ -13,6 +13,8 @@ pub struct Config { pub kubeconfig: Option, /// kubectl invocation for backups, e.g. ["/snap/bin/microk8s", "kubectl"]. pub kubectl: Vec, + /// containerd socket for local image scans (default: microk8s socket if present). + pub containerd_socket: Option, /// Scratch directory for backup archives. pub work_dir: std::path::PathBuf, pub bind: SocketAddr, @@ -47,6 +49,12 @@ impl Config { vec!["kubectl".into()] } }), + containerd_socket: env("CONTAINERD_ADDRESS").or_else(|| { + let microk8s = "/var/snap/microk8s/common/run/containerd.sock"; + std::path::Path::new(microk8s) + .exists() + .then(|| microk8s.to_string()) + }), work_dir: env("WORK_DIR") .map(Into::into) .unwrap_or_else(|| "data/work".into()), diff --git a/backend/crates/api/src/lib.rs b/backend/crates/api/src/lib.rs index 7158cf0..b84e48e 100644 --- a/backend/crates/api/src/lib.rs +++ b/backend/crates/api/src/lib.rs @@ -89,7 +89,10 @@ impl AppState { inspector: Arc::new(DebianInspector::new(runner.clone())), updater: Arc::new(DebianUpdater::new(runner.clone())), cluster: Arc::new(KubeGateway::new(cfg.kubeconfig.clone())), - scanner: Arc::new(TrivyScanner::new(runner.clone())), + scanner: Arc::new(match &cfg.containerd_socket { + Some(sock) => TrivyScanner::new(runner.clone()).with_containerd(sock), + None => TrivyScanner::new(runner.clone()), + }), storage: Arc::new(CommandBackupStorage::new(runner.clone())), collector: Arc::new(KubeBackupCollector::new( runner.clone(), diff --git a/backend/crates/api/src/test_support.rs b/backend/crates/api/src/test_support.rs index bf69806..8941d27 100644 --- a/backend/crates/api/src/test_support.rs +++ b/backend/crates/api/src/test_support.rs @@ -18,6 +18,7 @@ pub fn test_config() -> Config { fake_host: true, kubeconfig: None, kubectl: vec!["kubectl".into()], + containerd_socket: None, work_dir: std::env::temp_dir().join(format!("monitoring-test-{}", uuid::Uuid::new_v4())), bind: "127.0.0.1:0".parse().unwrap(), bootstrap_admin: None, diff --git a/backend/crates/infrastructure/src/trivy.rs b/backend/crates/infrastructure/src/trivy.rs index 4a1ab50..26edf39 100644 --- a/backend/crates/infrastructure/src/trivy.rs +++ b/backend/crates/infrastructure/src/trivy.rs @@ -10,11 +10,21 @@ use crate::host::CommandRunner; pub struct TrivyScanner { runner: Arc, + containerd: Option, } impl TrivyScanner { pub fn new(runner: Arc) -> Self { - Self { runner } + Self { + runner, + containerd: None, + } + } + + /// Scan images from this containerd socket first, falling back to the registry. + pub fn with_containerd(mut self, socket: &str) -> Self { + self.containerd = Some(socket.to_string()); + self } } @@ -88,7 +98,12 @@ impl TrivyScanner { out: &dyn LineSink, ) -> Result, DomainError> { out.line(&format!("$ trivy {}", args.join(" "))); - let res = self.runner.run("trivy", args).await?; + let env: Vec<(&str, &str)> = self + .containerd + .iter() + .map(|s| ("CONTAINERD_ADDRESS", s.as_str())) + .collect(); + let res = self.runner.run_env("trivy", args, &env).await?; if !res.success { let tail: Vec<&str> = res.stderr.lines().rev().take(5).collect(); for l in tail.into_iter().rev() { @@ -148,6 +163,9 @@ impl VulnerabilityScanner for TrivyScanner { ) -> Result, DomainError> { let mut args = vec!["image"]; args.extend(COMMON); + if self.containerd.is_some() { + args.extend(["--image-src", "containerd,remote"]); + } args.push(image); self.scan(&args, out).await } @@ -351,6 +369,76 @@ mod tests { fn line(&self, _: &str) {} } + #[tokio::test] + async fn image_scan_prefers_local_containerd_when_configured() { + use std::sync::Mutex; + #[derive(Default)] + struct Env(Mutex, Vec<(String, String)>)>>); + #[async_trait] + impl CommandRunner for Env { + async fn run(&self, p: &str, a: &[&str]) -> Result { + self.run_env(p, a, &[]).await + } + async fn run_env( + &self, + _: &str, + a: &[&str], + env: &[(&str, &str)], + ) -> Result { + self.0.lock().unwrap().push(( + a.iter().map(|s| s.to_string()).collect(), + env.iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect(), + )); + Ok(crate::host::Output { + stdout: "{}".into(), + success: true, + ..Default::default() + }) + } + async fn run_to_file( + &self, + _: &str, + _: &[&str], + _: &std::path::Path, + ) -> Result { + unreachable!() + } + async fn read_file(&self, _: &str) -> Result, DomainError> { + Ok(None) + } + async fn run_streaming( + &self, + _: &str, + _: &[&str], + _: &dyn LineSink, + ) -> Result { + Ok(true) + } + } + let r = Arc::new(Env::default()); + let s = TrivyScanner::new(r.clone()) + .with_containerd("/var/snap/microk8s/common/run/containerd.sock"); + s.scan_image("x/y:1", &Sink).await.unwrap(); + s.scan_os(&Sink).await.unwrap(); + let calls = r.0.lock().unwrap(); + let (args, env) = &calls[0]; + let i = args.iter().position(|a| a == "--image-src").unwrap(); + assert_eq!(args[i + 1], "containerd,remote"); + assert_eq!( + env[0], + ( + "CONTAINERD_ADDRESS".to_string(), + "/var/snap/microk8s/common/run/containerd.sock".to_string() + ) + ); + assert!( + !calls[1].0.contains(&"--image-src".to_string()), + "rootfs scan has no image source" + ); + } + #[tokio::test] async fn scanner_invokes_trivy_and_parses() { let s = TrivyScanner::new(Arc::new(Canned));