Trivy: scan images from the local containerd store before the registry
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@ -13,6 +13,8 @@ pub struct Config {
|
||||
pub kubeconfig: Option<String>,
|
||||
/// kubectl invocation for backups, e.g. ["/snap/bin/microk8s", "kubectl"].
|
||||
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.
|
||||
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()),
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -10,11 +10,21 @@ use crate::host::CommandRunner;
|
||||
|
||||
pub struct TrivyScanner {
|
||||
runner: Arc<dyn CommandRunner>,
|
||||
containerd: Option<String>,
|
||||
}
|
||||
|
||||
impl TrivyScanner {
|
||||
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,
|
||||
) -> Result<Vec<RawFinding>, 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<Vec<RawFinding>, 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<(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]
|
||||
async fn scanner_invokes_trivy_and_parses() {
|
||||
let s = TrivyScanner::new(Arc::new(Canned));
|
||||
|
||||
Reference in New Issue
Block a user