diff --git a/Cargo.toml b/Cargo.toml index 14e4cfe..e80faa7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -187,6 +187,7 @@ jsonwebtoken = "9.3.1" ipnet = "2.11.0" jira_query = "1.6.0" oci-distribution = "0.11.0" +walkdir = "2.5.0" [dependencies.tikv-jemallocator] version = "0.6" diff --git a/src/findings_store.rs b/src/findings_store.rs index 7d3cd76..5972490 100644 --- a/src/findings_store.rs +++ b/src/findings_store.rs @@ -52,6 +52,7 @@ pub struct FindingsStore { bloom_items: usize, blob_meta: FxHashMap>, origin_meta: FxHashMap>, + docker_images: FxHashMap, } impl FindingsStore { pub fn new(clone_dir: PathBuf) -> Self { @@ -69,6 +70,7 @@ impl FindingsStore { clone_dir, seen_bloom, bloom_items: 0, + docker_images: FxHashMap::default(), } } @@ -286,6 +288,13 @@ impl FindingsStore { self.clone_dir.clone() } + pub fn register_docker_image(&mut self, dir: PathBuf, image: String) { + self.docker_images.insert(dir, image); + } + + pub fn docker_images(&self) -> &FxHashMap { + &self.docker_images + } pub fn get_finding_data_iter( &self, diff --git a/src/reporter.rs b/src/reporter.rs index f6a3331..91fcbd1 100644 --- a/src/reporter.rs +++ b/src/reporter.rs @@ -140,6 +140,21 @@ impl DetailsReporter { } } + fn docker_display_path(&self, path: &std::path::Path) -> Option { + let ds = self.datastore.lock().ok()?; + for (dir, image) in ds.docker_images().iter() { + if path.starts_with(dir) { + let rel = path.strip_prefix(dir).ok()?; + let mut rel_str = rel.display().to_string(); + rel_str = rel_str.replace(".decomp.tar!", ".tar.gz => "); + rel_str = rel_str.replace(".tar!", ".tar => "); + rel_str = rel_str.replace('!', " => "); + return Some(format!("{} => {}", image, rel_str)); + } + } + None + } + fn gather_findings(&self) -> Result> { let metadata_list = self.get_finding_data()?; let all_matches = self.get_filtered_matches()?; diff --git a/src/reporter/json_format.rs b/src/reporter/json_format.rs index 4132def..6916337 100644 --- a/src/reporter/json_format.rs +++ b/src/reporter/json_format.rs @@ -103,6 +103,8 @@ impl DetailsReporter { if let Origin::File(e) = origin { if let Some(url) = self.jira_issue_url(&e.path, args) { Some(url) + } else if let Some(mapped) = self.docker_display_path(&e.path) { + Some(mapped) } else { Some(e.path.display().to_string()) } @@ -252,6 +254,8 @@ impl DetailsReporter { if let Origin::File(e) = origin { if let Some(url) = self.jira_issue_url(&e.path, args) { Some(url) + } else if let Some(mapped) = self.docker_display_path(&e.path) { + Some(mapped) } else { Some(e.path.display().to_string()) } diff --git a/src/reporter/pretty_format.rs b/src/reporter/pretty_format.rs index 5960538..ffa7cf0 100644 --- a/src/reporter/pretty_format.rs +++ b/src/reporter/pretty_format.rs @@ -216,6 +216,8 @@ impl<'a> Display for PrettyFinding<'a> { Origin::File(e) => { let display_path = if let Some(url) = reporter.jira_issue_url(&e.path, args) { url + } else if let Some(mapped) = reporter.docker_display_path(&e.path) { + mapped } else { e.path.display().to_string() }; diff --git a/src/scanner/docker.rs b/src/scanner/docker.rs index dd70bbe..4a61ac3 100644 --- a/src/scanner/docker.rs +++ b/src/scanner/docker.rs @@ -1,12 +1,16 @@ -use std::io::Write; +use std::fs::File; +use std::io::{Read, Write}; use std::path::{Path, PathBuf}; +use std::process::Command; use std::time::Duration; -use anyhow::{Context, Result}; +use anyhow::{anyhow, Context, Result}; +use indicatif::{ProgressBar, ProgressStyle}; use oci_distribution::client::{linux_amd64_resolver, Client, ClientConfig}; use oci_distribution::{secrets::RegistryAuth, Reference}; -use indicatif::{ProgressBar, ProgressStyle}; +use sha2::{Digest, Sha256}; use tracing::debug; +use walkdir::WalkDir; use crate::decompress::decompress_file; @@ -17,19 +21,80 @@ impl Docker { Docker } + fn try_save_local_image(&self, image: &str, out_dir: &Path, use_progress: bool) -> Result<()> { + let docker = Command::new("docker") + .args(["image", "inspect", image]) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .status(); + + if !matches!(docker, Ok(s) if s.success()) { + return Err(anyhow!("image not local")); + } + + let pb = if use_progress { + let style = ProgressStyle::with_template("{spinner} {msg} {pos}/{len}") + .expect("progress template"); + let pb = ProgressBar::new(0).with_style(style); + pb.enable_steady_tick(Duration::from_millis(100)); + pb + } else { + ProgressBar::hidden() + }; + pb.set_message(format!("saving local {image}")); + + std::fs::create_dir_all(out_dir)?; + let tar_path = out_dir.join("local_image.tar"); + let status = Command::new("docker") + .args(["image", "save", image, "-o", tar_path.to_str().unwrap()]) + .status() + .with_context(|| "running docker save")?; + if !status.success() { + pb.finish_with_message("docker save failed"); + return Err(anyhow!("failed to save local image")); + } + + pb.set_message("extracting layers"); + decompress_file(&tar_path, Some(out_dir))?; + + let mut layer_paths = Vec::new(); + for entry in WalkDir::new(out_dir) { + let entry = entry?; + if entry.file_name() == "layer.tar" { + layer_paths.push(entry.path().to_path_buf()); + } + } + + pb.set_length(layer_paths.len() as u64); + for p in layer_paths { + let mut data = Vec::new(); + File::open(&p)?.read_to_end(&mut data)?; + let digest = format!("{:x}", Sha256::digest(&data)); + let new_path = out_dir.join(format!("layer_{digest}.tar")); + std::fs::rename(&p, &new_path)?; + pb.inc(1); + } + + pb.finish_with_message(format!("saved {image}")); + Ok(()) + } + pub async fn save_image_to_dir( &self, image: &str, out_dir: &Path, use_progress: bool, ) -> Result<()> { + if self.try_save_local_image(image, out_dir, use_progress).is_ok() { + return Ok(()); + } let reference: Reference = image.parse().with_context(|| format!("invalid image reference {image}"))?; debug!("Pulling {image}"); let pb = if use_progress { - let style = ProgressStyle::with_template("{spinner} {msg}") + let style = ProgressStyle::with_template("{spinner} {msg} {pos}/{len}") .expect("progress template"); - let pb = ProgressBar::new_spinner().with_style(style); + let pb = ProgressBar::new(0).with_style(style); pb.enable_steady_tick(Duration::from_millis(100)); pb.set_message(format!("pulling {image}")); pb @@ -53,7 +118,7 @@ impl Docker { pb.set_message("extracting layers"); std::fs::create_dir_all(out_dir)?; - for (idx, layer) in pulled.layers.into_iter().enumerate() { + for layer in pulled.layers.into_iter() { let ext = match layer.media_type.as_str() { oci_distribution::manifest::IMAGE_LAYER_GZIP_MEDIA_TYPE | oci_distribution::manifest::IMAGE_DOCKER_LAYER_GZIP_MEDIA_TYPE => "tar.gz", @@ -61,11 +126,11 @@ impl Docker { | oci_distribution::manifest::IMAGE_DOCKER_LAYER_TAR_MEDIA_TYPE => "tar", _ => "bin", }; - let file_name = format!("layer_{idx}.{ext}"); + let digest = layer.sha256_digest(); + let file_name = format!("layer_{digest}.{ext}"); let tmp_path = out_dir.join(file_name); let mut tmp = std::fs::File::create(&tmp_path)?; tmp.write_all(&layer.data)?; - decompress_file(&tmp_path, Some(out_dir))?; pb.inc(1); } pb.finish_with_message(format!("saved {image}")); @@ -77,7 +142,7 @@ pub async fn save_docker_images( images: &[String], clone_root: &Path, use_progress: bool, -) -> Result> { +) -> Result> { let docker = Docker::new(); let mut dirs = Vec::new(); for image in images { @@ -87,7 +152,7 @@ pub async fn save_docker_images( .save_image_to_dir(image, &out_dir, use_progress) .await .with_context(|| format!("saving image {image}"))?; - dirs.push(out_dir); + dirs.push((out_dir, image.clone())); } Ok(dirs) } diff --git a/src/scanner/runner.rs b/src/scanner/runner.rs index 4dec5e7..52c8004 100644 --- a/src/scanner/runner.rs +++ b/src/scanner/runner.rs @@ -80,9 +80,16 @@ pub async fn run_async_scan( progress_enabled, ) .await?; - input_roots.extend(docker_dirs); + for (dir, img) in docker_dirs { + { + let mut ds = datastore.lock().unwrap(); + ds.register_docker_image(dir.clone(), img); + } + input_roots.push(dir); + } } + if input_roots.is_empty() { bail!("No inputs to scan"); }