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