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::{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        "DL3006",
15        Severity::Warning,
16        "Always tag the version of an image explicitly",
17        |state, line, instr, _shell| {
18            if let Instruction::From(base) = instr {
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#[cfg(test)]
64mod tests {
65    use super::*;
66    use crate::analyzer::hadolint::parser::instruction::{BaseImage, ImageAlias};
67    use crate::analyzer::hadolint::rules::Rule;
68
69    #[test]
70    fn test_tagged_image() {
71        let rule = rule();
72        let mut state = RuleState::new();
73
74        let mut base = BaseImage::new("ubuntu");
75        base.tag = Some("20.04".to_string());
76        let instr = Instruction::From(base);
77
78        rule.check(&mut state, 1, &instr, None);
79        assert!(state.failures.is_empty());
80    }
81
82    #[test]
83    fn test_untagged_image() {
84        let rule = rule();
85        let mut state = RuleState::new();
86
87        let instr = Instruction::From(BaseImage::new("ubuntu"));
88        rule.check(&mut state, 1, &instr, None);
89        assert_eq!(state.failures.len(), 1);
90        assert_eq!(state.failures[0].code.as_str(), "DL3006");
91    }
92
93    #[test]
94    fn test_scratch_image() {
95        let rule = rule();
96        let mut state = RuleState::new();
97
98        let instr = Instruction::From(BaseImage::new("scratch"));
99        rule.check(&mut state, 1, &instr, None);
100        assert!(state.failures.is_empty());
101    }
102
103    #[test]
104    fn test_stage_reference() {
105        let rule = rule();
106        let mut state = RuleState::new();
107
108        // First stage with alias
109        let mut base1 = BaseImage::new("node");
110        base1.tag = Some("18".to_string());
111        base1.alias = Some(ImageAlias::new("builder"));
112        let instr1 = Instruction::From(base1);
113        rule.check(&mut state, 1, &instr1, None);
114
115        // Second stage referencing first
116        let instr2 = Instruction::From(BaseImage::new("builder"));
117        rule.check(&mut state, 10, &instr2, None);
118
119        assert!(state.failures.is_empty());
120    }
121
122    #[test]
123    fn test_variable_image() {
124        let rule = rule();
125        let mut state = RuleState::new();
126
127        let instr = Instruction::From(BaseImage::new("${BASE_IMAGE}"));
128        rule.check(&mut state, 1, &instr, None);
129        assert!(state.failures.is_empty());
130    }
131}