forked from mirrors/kingfisher
WIP: Adding support for scanning Docker images
This commit is contained in:
parent
bb78d90067
commit
9a3fabdbf2
16 changed files with 137 additions and 12 deletions
|
|
@ -4,6 +4,7 @@ All notable changes to this project will be documented in this file.
|
|||
|
||||
## [1.27.0]
|
||||
- Added Buildkite rule
|
||||
- Added support for scanning Docker images via `--docker-image`
|
||||
|
||||
## [1.26.0]
|
||||
- Added rule for ElevenLabs
|
||||
|
|
|
|||
|
|
@ -186,6 +186,7 @@ globset = "0.4.16"
|
|||
jsonwebtoken = "9.3.1"
|
||||
ipnet = "2.11.0"
|
||||
jira_query = "1.6.0"
|
||||
oci-distribution = "0.11.0"
|
||||
|
||||
[dependencies.tikv-jemallocator]
|
||||
version = "0.6"
|
||||
|
|
|
|||
|
|
@ -197,6 +197,14 @@ kingfisher scan /path/to/repo --format sarif --output findings.sarif
|
|||
cat /path/to/file.py | kingfisher scan -
|
||||
```
|
||||
|
||||
### Scan a Docker image (without Docker installed)
|
||||
|
||||
```bash
|
||||
kingfisher scan --docker-image ubuntu:latest
|
||||
```
|
||||
|
||||
### Sc
|
||||
|
||||
### Scan using a rule _family_ with one flag
|
||||
|
||||
_(prefix matching: `--rule kingfisher.aws` loads `kingfisher.aws._`)\*
|
||||
|
|
|
|||
|
|
@ -27,4 +27,5 @@ rules:
|
|||
- type: StatusMatch
|
||||
status: [200]
|
||||
- type: WordMatch
|
||||
words: ['"uuid"', '"user"']
|
||||
words: ['"uuid"', '"user"']
|
||||
|
||||
|
|
@ -26,7 +26,8 @@ pub struct InputSpecifierArgs {
|
|||
"git_url",
|
||||
"all_github_organizations",
|
||||
"all_gitlab_groups",
|
||||
"jira_url"
|
||||
"jira_url",
|
||||
"docker_image"
|
||||
]),
|
||||
value_hint = ValueHint::AnyPath
|
||||
)]
|
||||
|
|
@ -97,6 +98,11 @@ pub struct InputSpecifierArgs {
|
|||
#[arg(long, default_value_t = 100)]
|
||||
pub max_results: usize,
|
||||
|
||||
/// Docker/OCI images to scan (no local Docker required)
|
||||
#[arg(long = "docker-image")]
|
||||
pub docker_image: Vec<String>,
|
||||
|
||||
|
||||
/// Select how to clone Git repositories
|
||||
#[arg(long, default_value_t=GitCloneMode::Bare, alias="git-clone-mode")]
|
||||
pub git_clone: GitCloneMode,
|
||||
|
|
|
|||
|
|
@ -281,7 +281,9 @@ fn create_default_scan_args() -> cli::commands::scan::ScanArgs {
|
|||
|
||||
jira_url: None,
|
||||
jql: None,
|
||||
max_results: 50,
|
||||
max_results: 100,
|
||||
// Docker image scanning
|
||||
docker_image: Vec::new(),
|
||||
|
||||
// git clone / history options
|
||||
git_clone: GitCloneMode::Bare,
|
||||
|
|
|
|||
|
|
@ -428,7 +428,9 @@ mod tests {
|
|||
// Jira options
|
||||
jira_url: None,
|
||||
jql: None,
|
||||
max_results: 50,
|
||||
max_results: 100,
|
||||
// Docker image scanning
|
||||
docker_image: Vec::new(),
|
||||
// clone / history options
|
||||
git_clone: GitCloneMode::Bare,
|
||||
git_history: GitHistoryMode::Full,
|
||||
|
|
|
|||
|
|
@ -344,7 +344,9 @@ fn test_pretty_format_with_nan_entropy_panics() {
|
|||
// Jira options
|
||||
jira_url: None,
|
||||
jql: None,
|
||||
max_results: 50,
|
||||
max_results: 100,
|
||||
// Docker image scanning
|
||||
docker_image: Vec::new(),
|
||||
// git clone / history options
|
||||
git_clone: GitCloneMode::Bare,
|
||||
git_history: GitHistoryMode::Full,
|
||||
|
|
|
|||
77
src/scanner/docker.rs
Normal file
77
src/scanner/docker.rs
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use oci_distribution::client::{linux_amd64_resolver, Client, ClientConfig};
|
||||
use oci_distribution::{secrets::RegistryAuth, Reference};
|
||||
use tracing::debug;
|
||||
|
||||
use crate::decompress::decompress_file;
|
||||
|
||||
pub struct Docker;
|
||||
|
||||
impl Docker {
|
||||
pub fn new() -> Self {
|
||||
Docker
|
||||
}
|
||||
|
||||
pub async fn save_image_to_dir(&self, image: &str, out_dir: &Path) -> Result<()> {
|
||||
let reference: Reference =
|
||||
image.parse().with_context(|| format!("invalid image reference {image}"))?;
|
||||
debug!("Pulling {image}");
|
||||
let mut client = Client::new(ClientConfig {
|
||||
platform_resolver: Some(Box::new(linux_amd64_resolver)),
|
||||
..Default::default()
|
||||
});
|
||||
let auth = RegistryAuth::Anonymous;
|
||||
let accepted = vec![
|
||||
oci_distribution::manifest::IMAGE_LAYER_MEDIA_TYPE,
|
||||
oci_distribution::manifest::IMAGE_LAYER_GZIP_MEDIA_TYPE,
|
||||
oci_distribution::manifest::IMAGE_DOCKER_LAYER_TAR_MEDIA_TYPE,
|
||||
oci_distribution::manifest::IMAGE_DOCKER_LAYER_GZIP_MEDIA_TYPE,
|
||||
];
|
||||
let image = client.pull(&reference, &auth, accepted).await?;
|
||||
|
||||
std::fs::create_dir_all(out_dir)?;
|
||||
for (idx, layer) in image.layers.into_iter().enumerate() {
|
||||
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",
|
||||
oci_distribution::manifest::IMAGE_LAYER_MEDIA_TYPE
|
||||
| oci_distribution::manifest::IMAGE_DOCKER_LAYER_TAR_MEDIA_TYPE => "tar",
|
||||
_ => "bin",
|
||||
};
|
||||
let file_name = format!("layer_{idx}.{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))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn save_docker_images(images: &[String], clone_root: &Path) -> Result<Vec<PathBuf>> {
|
||||
let docker = Docker::new();
|
||||
let mut dirs = Vec::new();
|
||||
for image in images {
|
||||
let dir_name = image.replace(['/', ':'], "_");
|
||||
let out_dir = clone_root.join(format!("docker_{dir_name}"));
|
||||
docker
|
||||
.save_image_to_dir(image, &out_dir)
|
||||
.await
|
||||
.with_context(|| format!("saving image {image}"))?;
|
||||
dirs.push(out_dir);
|
||||
}
|
||||
Ok(dirs)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn docker_struct_new() {
|
||||
let _ = Docker::new();
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,9 @@ pub(crate) use enumerate::enumerate_filesystem_inputs;
|
|||
pub(crate) use repos::{clone_or_update_git_repos, enumerate_github_repos};
|
||||
pub use runner::{load_and_record_rules, run_async_scan, run_scan};
|
||||
pub(crate) use validation::run_secret_validation;
|
||||
pub(crate) use docker::save_docker_images;
|
||||
|
||||
mod docker;
|
||||
mod enumerate;
|
||||
mod processing;
|
||||
mod repos;
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ use crate::{
|
|||
scanner::{
|
||||
clone_or_update_git_repos, enumerate_filesystem_inputs, enumerate_github_repos,
|
||||
repos::{enumerate_gitlab_repos, fetch_jira_issues},
|
||||
run_secret_validation,
|
||||
run_secret_validation, save_docker_images,
|
||||
summary::print_scan_summary,
|
||||
},
|
||||
};
|
||||
|
|
@ -68,6 +68,17 @@ pub async fn run_async_scan(
|
|||
let jira_dirs = fetch_jira_issues(args, global_args, &datastore).await?;
|
||||
input_roots.extend(jira_dirs);
|
||||
|
||||
// Save Docker images if specified
|
||||
if !args.input_specifier_args.docker_image.is_empty() {
|
||||
let clone_root = {
|
||||
let ds = datastore.lock().unwrap();
|
||||
ds.clone_root()
|
||||
};
|
||||
let docker_dirs =
|
||||
save_docker_images(&args.input_specifier_args.docker_image, &clone_root).await?;
|
||||
input_roots.extend(docker_dirs);
|
||||
}
|
||||
|
||||
if input_roots.is_empty() {
|
||||
bail!("No inputs to scan");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,7 +81,9 @@ rules:
|
|||
|
||||
jira_url: None,
|
||||
jql: None,
|
||||
max_results: 50,
|
||||
max_results: 100,
|
||||
// Docker image scanning
|
||||
docker_image: Vec::new(),
|
||||
// git clone / history options
|
||||
git_clone: GitCloneMode::Bare,
|
||||
git_history: GitHistoryMode::Full,
|
||||
|
|
|
|||
|
|
@ -68,7 +68,9 @@ fn test_github_remote_scan() -> Result<()> {
|
|||
|
||||
jira_url: None,
|
||||
jql: None,
|
||||
max_results: 50,
|
||||
max_results: 100,
|
||||
// Docker image scanning
|
||||
docker_image: Vec::new(),
|
||||
// git clone / history options
|
||||
git_clone: GitCloneMode::Bare,
|
||||
git_history: GitHistoryMode::Full,
|
||||
|
|
|
|||
|
|
@ -67,7 +67,9 @@ fn test_gitlab_remote_scan() -> Result<()> {
|
|||
|
||||
jira_url: None,
|
||||
jql: None,
|
||||
max_results: 50,
|
||||
max_results: 100,
|
||||
// Docker image scanning
|
||||
docker_image: Vec::new(),
|
||||
git_clone: GitCloneMode::Bare,
|
||||
git_history: GitHistoryMode::Full,
|
||||
scan_nested_repos: true,
|
||||
|
|
|
|||
|
|
@ -124,7 +124,9 @@ async fn test_validation_cache_and_depvars() -> Result<()> {
|
|||
|
||||
jira_url: None,
|
||||
jql: None,
|
||||
max_results: 50,
|
||||
max_results: 100,
|
||||
// Docker image scanning
|
||||
docker_image: Vec::new(),
|
||||
// git clone / history options
|
||||
git_clone: GitCloneMode::Bare,
|
||||
git_history: GitHistoryMode::Full,
|
||||
|
|
|
|||
|
|
@ -67,7 +67,9 @@ impl TestContext {
|
|||
|
||||
jira_url: None,
|
||||
jql: None,
|
||||
max_results: 50,
|
||||
max_results: 100,
|
||||
// Docker image scanning
|
||||
docker_image: Vec::new(),
|
||||
// git clone / history options
|
||||
git_clone: GitCloneMode::Bare,
|
||||
git_history: GitHistoryMode::Full,
|
||||
|
|
@ -135,7 +137,9 @@ impl TestContext {
|
|||
|
||||
jira_url: None,
|
||||
jql: None,
|
||||
max_results: 50,
|
||||
max_results: 100,
|
||||
// Docker image scanning
|
||||
docker_image: Vec::new(),
|
||||
// git clone / history options
|
||||
git_clone: GitCloneMode::Bare,
|
||||
git_history: GitHistoryMode::Full,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue