Trivy: scan images from the local containerd store before the registry
Some checks failed
CI / backend (push) Has been cancelled
CI / frontend (push) Has been cancelled
CI / ui (push) Has been cancelled

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Dennis Nemec
2026-09-02 23:18:05 +02:00
parent 6134a47ff2
commit a89395ae18
4 changed files with 103 additions and 3 deletions

View File

@ -13,6 +13,8 @@ pub struct Config {
pub kubeconfig: Option<String>, pub kubeconfig: Option<String>,
/// kubectl invocation for backups, e.g. ["/snap/bin/microk8s", "kubectl"]. /// kubectl invocation for backups, e.g. ["/snap/bin/microk8s", "kubectl"].
pub kubectl: Vec<String>, pub kubectl: Vec<String>,
/// containerd socket for local image scans (default: microk8s socket if present).
pub containerd_socket: Option<String>,
/// Scratch directory for backup archives. /// Scratch directory for backup archives.
pub work_dir: std::path::PathBuf, pub work_dir: std::path::PathBuf,
pub bind: SocketAddr, pub bind: SocketAddr,
@ -47,6 +49,12 @@ impl Config {
vec!["kubectl".into()] 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") work_dir: env("WORK_DIR")
.map(Into::into) .map(Into::into)
.unwrap_or_else(|| "data/work".into()), .unwrap_or_else(|| "data/work".into()),

View File

@ -89,7 +89,10 @@ impl AppState {
inspector: Arc::new(DebianInspector::new(runner.clone())), inspector: Arc::new(DebianInspector::new(runner.clone())),
updater: Arc::new(DebianUpdater::new(runner.clone())), updater: Arc::new(DebianUpdater::new(runner.clone())),
cluster: Arc::new(KubeGateway::new(cfg.kubeconfig.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())), storage: Arc::new(CommandBackupStorage::new(runner.clone())),
collector: Arc::new(KubeBackupCollector::new( collector: Arc::new(KubeBackupCollector::new(
runner.clone(), runner.clone(),

View File

@ -18,6 +18,7 @@ pub fn test_config() -> Config {
fake_host: true, fake_host: true,
kubeconfig: None, kubeconfig: None,
kubectl: vec!["kubectl".into()], kubectl: vec!["kubectl".into()],
containerd_socket: None,
work_dir: std::env::temp_dir().join(format!("monitoring-test-{}", uuid::Uuid::new_v4())), work_dir: std::env::temp_dir().join(format!("monitoring-test-{}", uuid::Uuid::new_v4())),
bind: "127.0.0.1:0".parse().unwrap(), bind: "127.0.0.1:0".parse().unwrap(),
bootstrap_admin: None, bootstrap_admin: None,

View File

@ -10,11 +10,21 @@ use crate::host::CommandRunner;
pub struct TrivyScanner { pub struct TrivyScanner {
runner: Arc<dyn CommandRunner>, runner: Arc<dyn CommandRunner>,
containerd: Option<String>,
} }
impl TrivyScanner { impl TrivyScanner {
pub fn new(runner: Arc<dyn CommandRunner>) -> Self { pub fn new(runner: Arc<dyn CommandRunner>) -> 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, out: &dyn LineSink,
) -> Result<Vec<RawFinding>, DomainError> { ) -> Result<Vec<RawFinding>, DomainError> {
out.line(&format!("$ trivy {}", args.join(" "))); 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 { if !res.success {
let tail: Vec<&str> = res.stderr.lines().rev().take(5).collect(); let tail: Vec<&str> = res.stderr.lines().rev().take(5).collect();
for l in tail.into_iter().rev() { for l in tail.into_iter().rev() {
@ -148,6 +163,9 @@ impl VulnerabilityScanner for TrivyScanner {
) -> Result<Vec<RawFinding>, DomainError> { ) -> Result<Vec<RawFinding>, DomainError> {
let mut args = vec!["image"]; let mut args = vec!["image"];
args.extend(COMMON); args.extend(COMMON);
if self.containerd.is_some() {
args.extend(["--image-src", "containerd,remote"]);
}
args.push(image); args.push(image);
self.scan(&args, out).await self.scan(&args, out).await
} }
@ -351,6 +369,76 @@ mod tests {
fn line(&self, _: &str) {} 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<(Vec<String>, Vec<(String, String)>)>>);
#[async_trait]
impl CommandRunner for Env {
async fn run(&self, p: &str, a: &[&str]) -> Result<crate::host::Output, DomainError> {
self.run_env(p, a, &[]).await
}
async fn run_env(
&self,
_: &str,
a: &[&str],
env: &[(&str, &str)],
) -> Result<crate::host::Output, DomainError> {
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<crate::host::Output, DomainError> {
unreachable!()
}
async fn read_file(&self, _: &str) -> Result<Option<String>, DomainError> {
Ok(None)
}
async fn run_streaming(
&self,
_: &str,
_: &[&str],
_: &dyn LineSink,
) -> Result<bool, DomainError> {
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] #[tokio::test]
async fn scanner_invokes_trivy_and_parses() { async fn scanner_invokes_trivy_and_parses() {
let s = TrivyScanner::new(Arc::new(Canned)); let s = TrivyScanner::new(Arc::new(Canned));