use crate::linter::{Diagnostic, LintResult, Severity, Span};
const ROOT_USERS: &[&str] = &["root", "0"];
pub fn check(source: &str) -> LintResult {
let mut result = LintResult::new();
let mut last_user_line = 0;
let mut last_user_value = String::new();
let mut has_user_directive = false;
let mut has_cmd_or_entrypoint = false;
let mut cmd_line = 0;
for (line_num, line) in source.lines().enumerate() {
let trimmed = line.trim();
let upper = trimmed.to_uppercase();
if upper.starts_with("USER ") {
has_user_directive = true;
last_user_line = line_num + 1;
last_user_value = trimmed[5..].trim().to_string();
let user_lower = last_user_value.to_lowercase();
if ROOT_USERS.contains(&user_lower.as_str()) {
let span = Span::new(line_num + 1, 1, line_num + 1, trimmed.len().min(80));
let diag = Diagnostic::new(
"DOCKER011",
Severity::Warning,
format!(
"USER {} runs container as root - consider non-root user (F069)",
last_user_value
),
span,
);
result.add(diag);
}
if let Ok(uid) = last_user_value.parse::<u32>() {
if uid == 0 {
let span = Span::new(line_num + 1, 1, line_num + 1, trimmed.len().min(80));
let diag = Diagnostic::new(
"DOCKER011",
Severity::Warning,
"USER 0 runs container as root - consider non-root UID (F069)".to_string(),
span,
);
result.add(diag);
}
}
}
if upper.starts_with("CMD ") || upper.starts_with("ENTRYPOINT ") {
has_cmd_or_entrypoint = true;
cmd_line = line_num + 1;
}
}
if has_cmd_or_entrypoint && !has_user_directive {
let span = Span::new(cmd_line, 1, cmd_line, 1);
let diag = Diagnostic::new(
"DOCKER011",
Severity::Warning,
"No USER directive - container will run as root (F069)".to_string(),
span,
);
result.add(diag);
}
if has_user_directive && ROOT_USERS.contains(&last_user_value.to_lowercase().as_str()) {
let span = Span::new(last_user_line, 1, last_user_line, 1);
let diag = Diagnostic::new(
"DOCKER011",
Severity::Warning,
"Final USER is root - consider switching to non-root before CMD (F069)".to_string(),
span,
);
result.add(diag);
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_F069_nonroot_user() {
let dockerfile = r#"FROM python:3.12
RUN useradd -r appuser
USER appuser
CMD ["python", "app.py"]"#;
let result = check(dockerfile);
assert!(
!result
.diagnostics
.iter()
.any(|d| d.message.contains("as root")),
"F069: Non-root user should not warn about root"
);
}
#[test]
fn test_F069_root_user() {
let dockerfile = r#"FROM python:3.12
USER root
CMD ["python", "app.py"]"#;
let result = check(dockerfile);
assert!(
result
.diagnostics
.iter()
.any(|d| d.message.contains("root")),
"F069: Should warn about root user"
);
}
#[test]
fn test_F069_uid_zero() {
let dockerfile = r#"FROM python:3.12
USER 0
CMD ["python", "app.py"]"#;
let result = check(dockerfile);
assert!(
result
.diagnostics
.iter()
.any(|d| d.message.contains("USER 0")),
"F069: Should warn about UID 0"
);
}
#[test]
fn test_F069_no_user_directive() {
let dockerfile = r#"FROM python:3.12
COPY app.py /
CMD ["python", "app.py"]"#;
let result = check(dockerfile);
assert!(
result
.diagnostics
.iter()
.any(|d| d.message.contains("No USER directive")),
"F069: Should warn about missing USER directive"
);
}
#[test]
fn test_F069_numeric_uid() {
let dockerfile = r#"FROM python:3.12
USER 1001
CMD ["python", "app.py"]"#;
let result = check(dockerfile);
assert!(
!result
.diagnostics
.iter()
.any(|d| d.message.contains("as root")),
"F069: Non-root UID should not warn"
);
}
}