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