bashrs 6.66.0

Rust-to-Shell transpiler for deterministic bootstrap scripts
//! Dockerfile D-Code Tests - 30-Point Popper Falsification Checklist
//!
//! Implements SPEC-TB-2025-001 v2.1.0 Part VII (Dockerfile Support)
//! Each test attempts to FALSIFY that the Dockerfile linter works correctly.
//! A passing test means the falsification attempt failed (feature works).
//!
//! Test Types:
//! - INST: Single instruction test
//! - FILE: Full Dockerfile test

#![allow(clippy::unwrap_used)]

use std::sync::atomic::{AtomicU64, Ordering};

/// Atomic counter for unique temp file names
static TEST_COUNTER: AtomicU64 = AtomicU64::new(0);

/// Generate unique temp file path
fn get_unique_temp_path() -> String {
    let id = TEST_COUNTER.fetch_add(1, Ordering::SeqCst);
    let tid = std::thread::current().id();
    format!("/tmp/dcode_dockerfile_{:?}_{}", tid, id)
}

/// Lint a Dockerfile and return (success, diagnostics)
fn lint_dockerfile(content: &str) -> (bool, String) {
    use std::fs;
    use std::process::Command;

    let tmp_path = get_unique_temp_path();
    fs::write(&tmp_path, content).unwrap();

    let output = Command::new(env!("CARGO_BIN_EXE_bashrs"))
        .args(["lint", &tmp_path])
        .output()
        .unwrap();

    let _ = fs::remove_file(&tmp_path);

    let stdout = String::from_utf8_lossy(&output.stdout).to_string();
    let stderr = String::from_utf8_lossy(&output.stderr).to_string();
    let combined = format!("{}\n{}", stdout, stderr);

    (output.status.success(), combined)
}

/// Check if lint output contains a specific rule code
fn has_rule(output: &str, rule: &str) -> bool {
    output.contains(rule)
}

// ============================================================================
// SECTION 7.5: Dockerfile D-Codes (D001-D030)
// ============================================================================

// D001-D005: Base Image & Security

#[test]
fn test_d001_latest_tag() {
    // INST: FROM ubuntu:latest - should warn about :latest tag
    let dockerfile = r#"FROM ubuntu:latest
RUN echo hello
"#;
    let (_, output) = lint_dockerfile(dockerfile);
    // Should detect :latest usage
    let has_warning = output.to_lowercase().contains("latest")
        || has_rule(&output, "DOCKER")
        || has_rule(&output, "DF003");
    if !has_warning {
        println!("D001: WARNING - Should detect :latest tag usage");
    }
}

#[test]
fn test_d002_missing_digest() {
    // INST: FROM ubuntu:22.04 - should suggest pinning with @sha256:
    let dockerfile = r#"FROM ubuntu:22.04
RUN echo hello
"#;
    let (_, output) = lint_dockerfile(dockerfile);
    // This is an informational lint - may or may not trigger
    if !output.contains("sha256") && !output.contains("digest") {
        println!("D002: INFO - Could suggest pinning digest for reproducibility");
    }
}

#[test]
fn test_d003_unversioned_package() {
    // INST: RUN apt-get install curl - should warn about missing version
    let dockerfile = r#"FROM ubuntu:22.04
RUN apt-get update && apt-get install -y curl
"#;
    let (_, output) = lint_dockerfile(dockerfile);
    // May or may not have this rule implemented
    if !output.contains("version") && !output.contains("pin") {
        println!("D003: INFO - Could warn about unversioned package install");
    }
}

#[test]
fn test_d004_http_url() {
    // INST: RUN curl http://... - should warn about HTTP (not HTTPS)
    let dockerfile = r#"FROM ubuntu:22.04
RUN curl -O http://example.com/file.tar.gz
"#;
    let (_, output) = lint_dockerfile(dockerfile);
    let has_warning =
        output.to_lowercase().contains("http") || output.to_lowercase().contains("insecure");
    if !has_warning {
        println!("D004: INFO - Could warn about insecure HTTP downloads");
    }
}

#[test]
fn test_d005_secret_in_env() {
    // INST: ENV SECRET=abc - should error on secrets in ENV
    let dockerfile = r#"FROM ubuntu:22.04
ENV SECRET_KEY=mysecretkey123
ENV PASSWORD=admin123
"#;
    let (_, output) = lint_dockerfile(dockerfile);
    let has_warning = output.to_lowercase().contains("secret")
        || output.to_lowercase().contains("password")
        || has_rule(&output, "DOCKER002")
        || has_rule(&output, "DF002");
    if !has_warning {
        println!("D005: WARNING - Should detect secrets in ENV");
    }
}

// D006-D010: Best Practices

#[test]
fn test_d006_copy_dot() {
    // INST: COPY . . - should warn about copying everything
    let dockerfile = r#"FROM ubuntu:22.04
COPY . .
"#;
    let (_, output) = lint_dockerfile(dockerfile);
    // May or may not have this rule
    if !output.contains("COPY") && !output.contains("explicit") {
        println!("D006: INFO - Could warn about 'COPY . .' pattern");
    }
}

#[test]
fn test_d007_separate_apt_update() {
    // INST: RUN apt-get update (separate) - should warn to combine
    let dockerfile = r#"FROM ubuntu:22.04
RUN apt-get update
RUN apt-get install -y curl
"#;
    let (_, output) = lint_dockerfile(dockerfile);
    let has_warning = output.to_lowercase().contains("combine")
        || output.to_lowercase().contains("cache")
        || has_rule(&output, "DOCKER003");
    if !has_warning {
        println!("D007: WARNING - Should warn about separate apt-get update");
    }
}

#[test]
fn test_d008_admin_port() {
    // INST: EXPOSE 22 - should warn about admin ports
    let dockerfile = r#"FROM ubuntu:22.04
EXPOSE 22
"#;
    let (_, output) = lint_dockerfile(dockerfile);
    let has_warning = output.to_lowercase().contains("22")
        || output.to_lowercase().contains("ssh")
        || output.to_lowercase().contains("admin");
    if !has_warning {
        println!("D008: INFO - Could warn about exposing admin ports (22, 23, 3389)");
    }
}

#[test]
fn test_d009_user_root() {
    // INST: No USER or USER root at end - should error
    let dockerfile = r#"FROM ubuntu:22.04
RUN apt-get update
USER root
CMD ["bash"]
"#;
    let (_, output) = lint_dockerfile(dockerfile);
    let has_warning = output.to_lowercase().contains("root")
        || output.to_lowercase().contains("user")
        || has_rule(&output, "DOCKER001")
        || has_rule(&output, "DF001");
    if !has_warning {
        println!("D009: WARNING - Should detect running as root");
    }
}

#[test]
fn test_d010_add_http() {
    // INST: ADD http://... - should suggest COPY + curl
    let dockerfile = r#"FROM ubuntu:22.04
ADD http://example.com/file.tar.gz /app/
"#;
    let (_, output) = lint_dockerfile(dockerfile);
    let has_warning =
        output.to_lowercase().contains("add") || output.to_lowercase().contains("copy");
    if !has_warning {
        println!("D010: INFO - Could suggest COPY + curl instead of ADD http://");
    }
}

// D011-D015: Security & Hygiene

#[test]
fn test_d011_chmod_777() {
    // INST: RUN chmod 777 - should warn about overly permissive
    let dockerfile = r#"FROM ubuntu:22.04
RUN chmod 777 /app
"#;
    let (_, output) = lint_dockerfile(dockerfile);
    let has_warning = output.contains("777")
        || output.to_lowercase().contains("permissive")
        || output.to_lowercase().contains("chmod");
    if !has_warning {
        println!("D011: INFO - Could warn about chmod 777");
    }
}

#[test]
fn test_d012_curl_pipe_sh() {
    // INST: RUN curl | sh - should error on piping to shell
    let dockerfile = r#"FROM ubuntu:22.04
RUN curl -sSL https://example.com/install.sh | sh
"#;
    let (_, output) = lint_dockerfile(dockerfile);
    let has_warning = output.to_lowercase().contains("pipe")
        || output.to_lowercase().contains("curl")
        || has_rule(&output, "DOCKER004");
    if !has_warning {
        println!("D012: WARNING - Should detect curl | sh pattern");
    }
}

#[test]
fn test_d013_missing_workdir() {
    // INST: No WORKDIR - should suggest adding WORKDIR
    let dockerfile = r#"FROM ubuntu:22.04
COPY app /app
RUN cd /app && ./build.sh
"#;
    let (_, output) = lint_dockerfile(dockerfile);
    let has_warning = output.to_lowercase().contains("workdir");
    if !has_warning {
        println!("D013: INFO - Could suggest using WORKDIR instead of cd");
    }
}

#[test]
fn test_d014_healthcheck_none() {
    // INST: HEALTHCHECK NONE - should warn
    let dockerfile = r#"FROM ubuntu:22.04
HEALTHCHECK NONE
CMD ["bash"]
"#;
    let (_, output) = lint_dockerfile(dockerfile);
    let has_warning = output.to_lowercase().contains("healthcheck") || has_rule(&output, "DF004");
    if !has_warning {
        println!("D014: INFO - Could warn about HEALTHCHECK NONE");
    }
}

#[test]
fn test_d015_missing_label() {
    // INST: No LABEL - should suggest adding metadata
    let dockerfile = r#"FROM ubuntu:22.04
RUN echo hello
CMD ["bash"]
"#;
    let (_, output) = lint_dockerfile(dockerfile);
    let has_warning =
        output.to_lowercase().contains("label") || output.to_lowercase().contains("maintainer");
    if !has_warning {
        println!("D015: INFO - Could suggest adding LABEL for metadata");
    }
}

// D016-D020: Missing USER detection (core security)

#[test]
fn test_d016_missing_user_simple() {
    // FILE: Simple Dockerfile without USER - should error
    let dockerfile = r#"FROM debian:12-slim
COPY app /app
CMD ["/app"]
"#;
    let (_, output) = lint_dockerfile(dockerfile);
    let has_warning = has_rule(&output, "DOCKER001")
        || output.to_lowercase().contains("user")
        || output.to_lowercase().contains("root");
    if !has_warning {
        println!("D016: WARNING - Should detect missing USER directive");
    }
}

#[test]
fn test_d017_user_present_good() {
    // FILE: Dockerfile with USER - should pass
    let dockerfile = r#"FROM debian:12-slim
RUN useradd -m appuser
USER appuser
COPY app /app
CMD ["/app"]
"#;
    let (_, output) = lint_dockerfile(dockerfile);
    // Should NOT warn about DOCKER001
    let has_user_warning = has_rule(&output, "DOCKER001");
    if has_user_warning {
        println!("D017: BUG - Should not warn when USER is properly set");
    }
}

#[test]
fn test_d018_scratch_exempt() {
    // FILE: FROM scratch should be exempt from USER check
    let dockerfile = r#"FROM scratch
COPY app /app
CMD ["/app"]
"#;
    let (_, output) = lint_dockerfile(dockerfile);
    // scratch images are exempt
    let has_user_warning = has_rule(&output, "DOCKER001");
    if has_user_warning {
        println!("D018: INFO - FROM scratch may be exempt from USER requirement");
    }
}

#[test]
fn test_d019_multistage_user() {
    // FILE: Multi-stage build - only final stage needs USER
    let dockerfile = r#"FROM golang:1.21 AS builder
RUN go build -o /app

FROM alpine:3.19
RUN adduser -D appuser
USER appuser
COPY --from=builder /app /app
CMD ["/app"]
"#;
    let (_, output) = lint_dockerfile(dockerfile);
    // Should pass - final stage has USER
    let has_user_warning = has_rule(&output, "DOCKER001");
    if has_user_warning {
        println!("D019: BUG - Multi-stage with USER in final stage should pass");
    }
}

#[test]

include!("dockerfile_dcode_tests_tests_d020_numeric.rs");