#![allow(clippy::unwrap_used)] #![allow(clippy::expect_used)]
use proptest::prelude::*;
fn purify_dockerfile_content(content: &str) -> String {
let mut purified = Vec::new();
let mut has_user_directive = false;
let lines: Vec<&str> = content.lines().collect();
for line in &lines {
if line.trim().starts_with("USER ") {
has_user_directive = true;
break;
}
}
let is_scratch = lines.iter().any(|l| {
let trimmed = l.trim();
trimmed.starts_with("FROM scratch") || trimmed == "FROM scratch"
});
for line in &lines {
let mut processed_line = line.to_string();
if line.trim().starts_with("FROM ") {
processed_line = pin_base_image_simple(line);
}
if line.trim().starts_with("ADD ") {
processed_line = convert_add_to_copy_simple(&processed_line);
}
if line.trim().starts_with("RUN ") && processed_line.contains("apt-get install") {
processed_line = add_no_install_recommends_simple(&processed_line);
}
if line.trim().starts_with("RUN ") {
processed_line = add_cleanup_simple(&processed_line);
}
purified.push(processed_line);
if !has_user_directive
&& !is_scratch
&& (line.trim().starts_with("CMD ") || line.trim().starts_with("ENTRYPOINT "))
{
purified.insert(purified.len() - 1, String::new());
purified.insert(
purified.len() - 1,
"# Security: Run as non-root user".to_string(),
);
purified.insert(
purified.len() - 1,
"RUN groupadd -r appuser && useradd -r -g appuser appuser".to_string(),
);
purified.insert(purified.len() - 1, "USER appuser".to_string());
has_user_directive = true;
}
}
purified.join("\n")
}
fn pin_base_image_simple(line: &str) -> String {
let trimmed = line.trim();
if !trimmed.starts_with("FROM ") {
return line.to_string();
}
let parts: Vec<&str> = trimmed.split_whitespace().collect();
if parts.len() < 2 {
return line.to_string();
}
let image_part = parts[1];
let (image_name, tag) = if let Some(colon_pos) = image_part.find(':') {
(&image_part[..colon_pos], Some(&image_part[colon_pos + 1..]))
} else {
(image_part, None)
};
let needs_pinning = tag.is_none() || tag == Some("latest");
if !needs_pinning {
return line.to_string();
}
let pinned_tag = match image_name {
"ubuntu" => "22.04",
"debian" => "12-slim",
"alpine" => "3.19",
"node" => "20-alpine",
"python" => "3.11-slim",
_ => return line.to_string(),
};
format!("FROM {}:{}", image_name, pinned_tag)
}
fn convert_add_to_copy_simple(line: &str) -> String {
let trimmed = line.trim();
let parts: Vec<&str> = trimmed.split_whitespace().collect();
if parts.len() < 2 {
return line.to_string();
}
let source = parts[1];
if source.starts_with("http://") || source.starts_with("https://") {
return line.to_string();
}
line.replacen("ADD ", "COPY ", 1)
}
fn add_no_install_recommends_simple(line: &str) -> String {
if line.contains("--no-install-recommends") {
return line.to_string();
}
if line.contains("apt-get install -y") {
line.replacen(
"apt-get install -y",
"apt-get install -y --no-install-recommends",
1,
)
} else {
line.to_string()
}
}
fn add_cleanup_simple(line: &str) -> String {
if line.contains("/var/lib/apt/lists") || line.contains("/var/cache/apk") {
return line.to_string();
}
if line.contains("apt-get install") || line.contains("apt install") {
return format!("{} && rm -rf /var/lib/apt/lists/*", line.trim_end());
}
if line.contains("apk add") {
return format!("{} && rm -rf /var/cache/apk/*", line.trim_end());
}
line.to_string()
}
fn dockerfile_from_instruction() -> impl Strategy<Value = String> {
prop_oneof![
Just("FROM ubuntu"),
Just("FROM ubuntu:22.04"),
Just("FROM ubuntu:latest"),
Just("FROM debian"),
Just("FROM debian:12-slim"),
Just("FROM alpine"),
Just("FROM alpine:3.19"),
Just("FROM scratch"),
Just("FROM node:20"),
Just("FROM python:3.11"),
]
.prop_map(|s| s.to_string())
}
fn dockerfile_run_instruction() -> impl Strategy<Value = String> {
prop_oneof![
Just("RUN apt-get update"),
Just("RUN apt-get install -y curl"),
Just("RUN apt-get install -y python3"),
Just("RUN apt-get install -y --no-install-recommends wget"),
Just("RUN apk add curl"),
Just("RUN apk add python3"),
Just("RUN echo hello"),
Just("RUN mkdir /app"),
]
.prop_map(|s| s.to_string())
}
fn dockerfile_copy_instruction() -> impl Strategy<Value = String> {
prop_oneof![
Just("COPY app.py /app/"),
Just("COPY . /app/"),
Just("ADD myfile.txt /tmp/"),
Just("ADD https://example.com/file.tar.gz /tmp/"),
]
.prop_map(|s| s.to_string())
}
fn dockerfile_cmd_instruction() -> impl Strategy<Value = String> {
prop_oneof![
Just("CMD [\"bash\"]"),
Just("CMD [\"python3\", \"app.py\"]"),
Just("CMD [\"sh\", \"-c\", \"echo hello\"]"),
]
.prop_map(|s| s.to_string())
}
fn dockerfile_content() -> impl Strategy<Value = String> {
(
dockerfile_from_instruction(),
prop::collection::vec(dockerfile_run_instruction(), 1..5),
prop::collection::vec(dockerfile_copy_instruction(), 0..3),
dockerfile_cmd_instruction(),
)
.prop_map(|(from, runs, copies, cmd)| {
let mut lines = vec![from];
lines.extend(runs);
lines.extend(copies);
lines.push("WORKDIR /app".to_string());
lines.push(cmd);
lines.join("\n")
})
}
proptest! {
#![proptest_config(ProptestConfig {
cases: 100, // Start with 100 for fast feedback, increase to 1000+ for release
max_shrink_iters: 100,
.. ProptestConfig::default()
})]
#[test]
fn prop_dockerfile_purify_is_deterministic(content in dockerfile_content()) {
let result1 = purify_dockerfile_content(&content);
let result2 = purify_dockerfile_content(&content);
prop_assert_eq!(
result1,
result2,
"Purification must be deterministic - same input produces same output"
);
}
#[test]
fn prop_dockerfile_purify_is_idempotent(content in dockerfile_content()) {
let purified_once = purify_dockerfile_content(&content);
let purified_twice = purify_dockerfile_content(&purified_once);
prop_assert_eq!(
purified_once,
purified_twice,
"Purification must be idempotent - purifying twice gives same result"
);
}
#[test]
fn prop_dockerfile_purify_preserves_non_empty(content in dockerfile_content()) {
prop_assume!(!content.trim().is_empty());
let purified = purify_dockerfile_content(&content);
prop_assert!(
!purified.trim().is_empty(),
"Purification of non-empty Dockerfile must produce non-empty output"
);
}
#[test]
fn prop_dockerfile_purify_preserves_from(content in dockerfile_content()) {
prop_assume!(content.contains("FROM "));
let purified = purify_dockerfile_content(&content);
prop_assert!(
purified.contains("FROM "),
"Purification must preserve FROM instruction"
);
}
#[test]
fn prop_dockerfile_purify_preserves_cmd(content in dockerfile_content()) {
prop_assume!(content.contains("CMD ") || content.contains("ENTRYPOINT "));
let purified = purify_dockerfile_content(&content);
prop_assert!(
purified.contains("CMD ") || purified.contains("ENTRYPOINT "),
"Purification must preserve CMD/ENTRYPOINT"
);
}
#[test]
fn prop_dockerfile_purify_no_version_downgrade(content in dockerfile_content()) {
let has_version = content.lines().any(|l| {
l.trim().starts_with("FROM ") && l.contains(':') && !l.contains(":latest")
});
if has_version {
let purified = purify_dockerfile_content(&content);
prop_assert!(
purified.lines().any(|l| l.trim().starts_with("FROM ") && l.contains(':')),
"Purification must not remove existing version pins"
);
}
}
#[test]
fn prop_dockerfile_purify_adds_valid_cleanup(content in dockerfile_content()) {
let purified = purify_dockerfile_content(&content);
for line in purified.lines() {
if line.contains("rm -rf") {
prop_assert!(
line.contains("/var/lib/apt/lists") || line.contains("/var/cache/apk"),
"Cleanup commands must target valid package cache directories"
);
}
}
}
#[test]
fn prop_dockerfile_purify_adds_user_when_needed(content in dockerfile_content()) {
let has_user = content.contains("USER ");
let is_scratch = content.contains("FROM scratch");
let has_cmd = content.contains("CMD ") || content.contains("ENTRYPOINT ");
if !has_user && !is_scratch && has_cmd {
let purified = purify_dockerfile_content(&content);
prop_assert!(
purified.contains("USER "),
"Purification must add USER directive when missing (except for scratch images)"
);
}
}
#[test]
fn prop_dockerfile_purify_adds_no_install_recommends(content in dockerfile_content()) {
if content.contains("apt-get install") {
let purified = purify_dockerfile_content(&content);
for line in purified.lines() {
if line.contains("apt-get install") {
prop_assert!(
line.contains("--no-install-recommends") || line.contains("apt-get update"),
"apt-get install must include --no-install-recommends"
);
}
}
}
}
#[test]
fn prop_dockerfile_purify_converts_add_to_copy(content in dockerfile_content()) {
let has_local_add = content.lines().any(|l| {
l.trim().starts_with("ADD ") && !l.contains("http://") && !l.contains("https://")
});
if has_local_add {
let purified = purify_dockerfile_content(&content);
for line in purified.lines() {
if line.trim().starts_with("ADD ") {
prop_assert!(
line.contains("http://") || line.contains("https://"),
"Local files should use COPY, not ADD"
);
}
}
}
}
}
#[test]
fn test_property_purify_never_panics_on_empty() {
let result = purify_dockerfile_content("");
assert_eq!(result, "");
}
#[test]
fn test_property_purify_never_panics_on_whitespace() {
let result = purify_dockerfile_content(" \n\n\t\n ");
assert!(result.trim().is_empty());
}
#[test]
fn test_property_purify_handles_comments() {
let content = "# This is a comment\n# Another comment";
let result = purify_dockerfile_content(content);
assert!(result.contains("# This is a comment"));
}
#[test]
fn test_property_purify_handles_invalid_syntax() {
let content = "INVALID INSTRUCTION\nFROM ubuntu";
let result = purify_dockerfile_content(content);
assert!(result.contains("FROM ubuntu"));
}