syncable_cli/analyzer/hadolint/rules/
dl3002.rs

1//! DL3002: Last USER should not be root
2//!
3//! Running as root in containers is a security risk. The last USER
4//! instruction should switch to a non-root user.
5
6use crate::analyzer::hadolint::parser::instruction::Instruction;
7use crate::analyzer::hadolint::rules::{custom_rule, CustomRule, RuleState};
8use crate::analyzer::hadolint::shell::ParsedShell;
9use crate::analyzer::hadolint::types::Severity;
10
11pub fn rule() -> CustomRule<impl Fn(&mut RuleState, u32, &Instruction, Option<&ParsedShell>) + Send + Sync> {
12    custom_rule(
13        "DL3002",
14        Severity::Warning,
15        "Last USER should not be root",
16        |state, line, instr, _shell| {
17            match instr {
18                Instruction::From(_) => {
19                    // Reset state for each stage
20                    state.data.set_bool("is_root", true);
21                    state.data.set_int("last_user_line", 0);
22                }
23                Instruction::User(user) => {
24                    let is_root = user == "root" || user == "0" || user.starts_with("root:");
25                    state.data.set_bool("is_root", is_root);
26                    state.data.set_int("last_user_line", line as i64);
27                }
28                _ => {}
29            }
30        },
31    )
32}
33
34/// Custom finalize implementation for DL3002.
35/// This is called manually in the lint process.
36pub fn finalize(state: RuleState) -> Vec<crate::analyzer::hadolint::types::CheckFailure> {
37    let mut failures = state.failures;
38
39    // Check if the last USER was root
40    if state.data.get_bool("is_root") {
41        let last_line = state.data.get_int("last_user_line");
42        if last_line > 0 {
43            failures.push(crate::analyzer::hadolint::types::CheckFailure::new(
44                "DL3002",
45                Severity::Warning,
46                "Last USER should not be root",
47                last_line as u32,
48            ));
49        }
50    }
51
52    failures
53}
54
55#[cfg(test)]
56mod tests {
57    use super::*;
58    use crate::analyzer::hadolint::parser::instruction::BaseImage;
59    use crate::analyzer::hadolint::rules::Rule;
60
61    #[test]
62    fn test_non_root_user() {
63        let rule = rule();
64        let mut state = RuleState::new();
65
66        let from = Instruction::From(BaseImage::new("ubuntu"));
67        let user = Instruction::User("appuser".to_string());
68
69        rule.check(&mut state, 1, &from, None);
70        rule.check(&mut state, 2, &user, None);
71
72        let failures = finalize(state);
73        assert!(failures.is_empty());
74    }
75
76    #[test]
77    fn test_root_user() {
78        let rule = rule();
79        let mut state = RuleState::new();
80
81        let from = Instruction::From(BaseImage::new("ubuntu"));
82        let user = Instruction::User("root".to_string());
83
84        rule.check(&mut state, 1, &from, None);
85        rule.check(&mut state, 2, &user, None);
86
87        let failures = finalize(state);
88        assert_eq!(failures.len(), 1);
89        assert_eq!(failures[0].code.as_str(), "DL3002");
90    }
91
92    #[test]
93    fn test_switch_from_root() {
94        let rule = rule();
95        let mut state = RuleState::new();
96
97        let from = Instruction::From(BaseImage::new("ubuntu"));
98        let user1 = Instruction::User("root".to_string());
99        let user2 = Instruction::User("appuser".to_string());
100
101        rule.check(&mut state, 1, &from, None);
102        rule.check(&mut state, 2, &user1, None);
103        rule.check(&mut state, 3, &user2, None);
104
105        let failures = finalize(state);
106        assert!(failures.is_empty());
107    }
108}