pub mod parse;
mod rules;
use std::path::Path;
use crate::Result;
use crate::findings::Findings;
const COMPOSE_FILENAMES: &[&str] = &[
"docker-compose.yml",
"docker-compose.yaml",
"compose.yml",
"compose.yaml",
];
pub fn scan(project_root: &Path) -> Result<Findings> {
let mut findings = Findings::new();
for name in COMPOSE_FILENAMES {
let path = project_root.join(name);
if !path.is_file() {
continue;
}
let body = match std::fs::read_to_string(&path) {
Ok(s) => s,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => continue,
Err(source) => return Err(crate::Error::Io { path, source }),
};
let compose = parse::parse(&path, &body)?;
let rel = Path::new(name);
for (svc_name, svc) in &compose.services {
findings.extend(rules::check_service(rel, svc_name, svc));
}
}
Ok(findings)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::findings::Severity;
type TestResult = std::result::Result<(), Box<dyn std::error::Error>>;
#[test]
fn no_compose_file_no_findings() -> TestResult {
let tmp = tempfile::tempdir()?;
assert!(scan(tmp.path())?.is_empty());
Ok(())
}
#[test]
fn finds_privileged_service() -> TestResult {
let tmp = tempfile::tempdir()?;
std::fs::write(
tmp.path().join("docker-compose.yml"),
r#"
services:
evil:
image: x
privileged: true
"#,
)?;
let f = scan(tmp.path())?;
assert!(
f.iter()
.any(|x| x.rule_id == "compose/privileged" && x.severity == Severity::Critical)
);
Ok(())
}
#[test]
fn picks_first_extension_in_lookup_order() -> TestResult {
let tmp = tempfile::tempdir()?;
std::fs::write(
tmp.path().join("docker-compose.yml"),
"services:\n a:\n image: x\n privileged: true\n",
)?;
std::fs::write(
tmp.path().join("compose.yml"),
"services:\n b:\n image: x\n network_mode: host\n",
)?;
let f = scan(tmp.path())?;
assert!(f.iter().any(|x| x.rule_id == "compose/privileged"));
assert!(f.iter().any(|x| x.rule_id == "compose/network_mode_host"));
Ok(())
}
}