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