WIP: Adding support for scanning Docker images

This commit is contained in:
Mick Grove 2025-07-27 14:59:19 -07:00
commit 29b97b4091
7 changed files with 114 additions and 11 deletions

View file

@ -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"

View file

@ -52,6 +52,7 @@ pub struct FindingsStore {
bloom_items: usize,
blob_meta: FxHashMap<BlobId, Arc<BlobMetadata>>,
origin_meta: FxHashMap<u64, Arc<OriginSet>>,
docker_images: FxHashMap<PathBuf, String>,
}
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<PathBuf, String> {
&self.docker_images
}
pub fn get_finding_data_iter(
&self,

View file

@ -140,6 +140,21 @@ impl DetailsReporter {
}
}
fn docker_display_path(&self, path: &std::path::Path) -> Option<String> {
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<Vec<Finding>> {
let metadata_list = self.get_finding_data()?;
let all_matches = self.get_filtered_matches()?;

View file

@ -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())
}

View file

@ -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()
};

View file

@ -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<Vec<PathBuf>> {
) -> Result<Vec<(PathBuf, String)>> {
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)
}

View file

@ -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");
}