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>,
|
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()),
|
||||||
|
|||||||
@ -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(),
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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));
|
||||||
|
|||||||
Reference in New Issue
Block a user