syncable_cli/analyzer/hadolint/rules/
dl3006.rs

1//! DL3006: Always tag the version of an image explicitly
2//!
3//! Images should be tagged to ensure reproducible builds.
4//! Using untagged images may result in different versions being pulled.
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        "DL3006",
14        Severity::Warning,
15        "Always tag the version of an image explicitly",
16        |state, line, instr, _shell| {
17            match instr {
18                Instruction::From(base) => {
19                    // Remember stage aliases
20                    if let Some(alias) = &base.alias {
21                        state.data.insert_to_set("aliases", alias.as_str());
22                    }
23
24                    // Check if image needs a tag
25                    let image_name = &base.image.name;
26
27                    // Skip check for:
28                    // 1. scratch image
29                    // 2. images with tags
30                    // 3. images with digests
31                    // 4. variable references
32                    // 5. references to previous build stages
33
34                    if base.is_scratch() {
35                        return;
36                    }
37
38                    if base.has_version() {
39                        return;
40                    }
41
42                    if base.is_variable() {
43                        return;
44                    }
45
46                    // Check if it's a reference to a previous stage
47                    if state.data.set_contains("aliases", image_name) {
48                        return;
49                    }
50
51                    // Image doesn't have a tag
52                    state.add_failure(
53                        "DL3006",
54                        Severity::Warning,
55                        "Always tag the version of an image explicitly",
56                        line,
57                    );
58                }
59                _ => {}
60            }
61        },
62    )
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68    use crate::analyzer::hadolint::parser::instruction::{BaseImage, ImageAlias};
69    use crate::analyzer::hadolint::rules::Rule;
70
71    #[test]
72    fn test_tagged_image() {
73        let rule = rule();
74        let mut state = RuleState::new();
75
76        let mut base = BaseImage::new("ubuntu");
77        base.tag = Some("20.04".to_string());
78        let instr = Instruction::From(base);
79
80        rule.check(&mut state, 1, &instr, None);
81        assert!(state.failures.is_empty());
82    }
83
84    #[test]
85    fn test_untagged_image() {
86        let rule = rule();
87        let mut state = RuleState::new();
88
89        let instr = Instruction::From(BaseImage::new("ubuntu"));
90        rule.check(&mut state, 1, &instr, None);
91        assert_eq!(state.failures.len(), 1);
92        assert_eq!(state.failures[0].code.as_str(), "DL3006");
93    }
94
95    #[test]
96    fn test_scratch_image() {
97        let rule = rule();
98        let mut state = RuleState::new();
99
100        let instr = Instruction::From(BaseImage::new("scratch"));
101        rule.check(&mut state, 1, &instr, None);
102        assert!(state.failures.is_empty());
103    }
104
105    #[test]
106    fn test_stage_reference() {
107        let rule = rule();
108        let mut state = RuleState::new();
109
110        // First stage with alias
111        let mut base1 = BaseImage::new("node");
112        base1.tag = Some("18".to_string());
113        base1.alias = Some(ImageAlias::new("builder"));
114        let instr1 = Instruction::From(base1);
115        rule.check(&mut state, 1, &instr1, None);
116
117        // Second stage referencing first
118        let instr2 = Instruction::From(BaseImage::new("builder"));
119        rule.check(&mut state, 10, &instr2, None);
120
121        assert!(state.failures.is_empty());
122    }
123
124    #[test]
125    fn test_variable_image() {
126        let rule = rule();
127        let mut state = RuleState::new();
128
129        let instr = Instruction::From(BaseImage::new("${BASE_IMAGE}"));
130        rule.check(&mut state, 1, &instr, None);
131        assert!(state.failures.is_empty());
132    }
133}