forked from mirrors/kingfisher
WIP: Adding support for scanning Docker images
This commit is contained in:
parent
2d8550aa15
commit
29b97b4091
7 changed files with 114 additions and 11 deletions
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()?;
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue