syncable_cli/analyzer/hadolint/rules/
dl3062.rs

1//! DL3062: COPY --from should reference a defined stage
2//!
3//! When using COPY --from, the source should be a defined build stage.
4
5use crate::analyzer::hadolint::parser::instruction::Instruction;
6use crate::analyzer::hadolint::rules::{CustomRule, RuleState, custom_rule};
7use crate::analyzer::hadolint::shell::ParsedShell;
8use crate::analyzer::hadolint::types::Severity;
9
10pub fn rule()
11-> CustomRule<impl Fn(&mut RuleState, u32, &Instruction, Option<&ParsedShell>) + Send + Sync> {
12    custom_rule(
13        "DL3062",
14        Severity::Warning,
15        "`COPY --from` should reference a defined build stage or an external image.",
16        |state, line, instr, _shell| {
17            match instr {
18                Instruction::From(base_image) => {
19                    // Track stage aliases
20                    if let Some(alias) = &base_image.alias {
21                        state
22                            .data
23                            .insert_to_set("stages", alias.as_str().to_string());
24                    }
25                    // Track stage count
26                    let count = state.data.get_int("stage_count");
27                    state.data.insert_to_set("stages", count.to_string());
28                    state.data.set_int("stage_count", count + 1);
29                }
30                Instruction::Copy(_, flags) => {
31                    if let Some(from) = &flags.from {
32                        let from_str = from.as_str();
33
34                        // It's valid if:
35                        // 1. It references a defined stage alias
36                        // 2. It references a stage by index
37                        // 3. It's an external image (contains / or . or : for tags)
38
39                        let is_stage_alias = state.data.set_contains("stages", from_str);
40                        let is_stage_index = from_str.parse::<usize>().is_ok();
41                        let is_external = from_str.contains('/')
42                            || from_str.contains('.')
43                            || from_str.contains(':');
44
45                        if !is_stage_alias && !is_stage_index && !is_external {
46                            state.add_failure("DL3062", Severity::Warning, "`COPY --from` should reference a defined build stage or an external image.", line);
47                        }
48                    }
49                }
50                _ => {}
51            }
52        },
53    )
54}
55
56#[cfg(test)]
57mod tests {
58    use super::*;
59    use crate::analyzer::hadolint::config::HadolintConfig;
60    use crate::analyzer::hadolint::lint::{LintResult, lint};
61
62    fn lint_dockerfile(content: &str) -> LintResult {
63        lint(content, &HadolintConfig::default())
64    }
65
66    #[test]
67    fn test_copy_from_defined_stage() {
68        let result = lint_dockerfile(
69            "FROM ubuntu:20.04 AS builder\nRUN echo hello\nFROM alpine:3.14\nCOPY --from=builder /app /app",
70        );
71        assert!(!result.failures.iter().any(|f| f.code.as_str() == "DL3062"));
72    }
73
74    #[test]
75    fn test_copy_from_stage_index() {
76        let result = lint_dockerfile(
77            "FROM ubuntu:20.04\nRUN echo hello\nFROM alpine:3.14\nCOPY --from=0 /app /app",
78        );
79        assert!(!result.failures.iter().any(|f| f.code.as_str() == "DL3062"));
80    }
81
82    #[test]
83    fn test_copy_from_external_image() {
84        let result =
85            lint_dockerfile("FROM ubuntu:20.04\nCOPY --from=nginx:latest /etc/nginx /etc/nginx");
86        assert!(!result.failures.iter().any(|f| f.code.as_str() == "DL3062"));
87    }
88
89    #[test]
90    fn test_copy_from_undefined_stage() {
91        let result = lint_dockerfile("FROM ubuntu:20.04\nCOPY --from=nonexistent /app /app");
92        assert!(result.failures.iter().any(|f| f.code.as_str() == "DL3062"));
93    }
94}