syncable_cli/analyzer/hadolint/rules/
dl3023.rs

1//! DL3023: COPY --from cannot reference its own FROM alias
2//!
3//! A COPY instruction cannot reference the current stage as the source.
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        "DL3023",
13        Severity::Error,
14        "`COPY --from` cannot reference its own `FROM` alias.",
15        |state, line, instr, _shell| {
16            match instr {
17                Instruction::From(base) => {
18                    // Track current stage alias
19                    if let Some(alias) = &base.alias {
20                        state.data.set_string("current_stage", alias.as_str());
21                    } else {
22                        state.data.strings.remove("current_stage");
23                    }
24                    // Track current stage index
25                    let stage_count = state.data.get_int("stage_count");
26                    state.data.set_int("current_stage_index", stage_count);
27                    state.data.set_int("stage_count", stage_count + 1);
28                }
29                Instruction::Copy(_, flags) => {
30                    if let Some(from) = &flags.from {
31                        // Check if referencing current stage
32                        let is_current_alias = state.data.get_string("current_stage")
33                            .map(|s| s == from)
34                            .unwrap_or(false);
35
36                        let is_current_index = from.parse::<i64>().ok()
37                            .map(|n| n == state.data.get_int("current_stage_index"))
38                            .unwrap_or(false);
39
40                        if is_current_alias || is_current_index {
41                            state.add_failure(
42                                "DL3023",
43                                Severity::Error,
44                                "`COPY --from` cannot reference its own `FROM` alias.",
45                                line,
46                            );
47                        }
48                    }
49                }
50                _ => {}
51            }
52        },
53    )
54}
55
56#[cfg(test)]
57mod tests {
58    use super::*;
59    use crate::analyzer::hadolint::lint::{lint, LintResult};
60    use crate::analyzer::hadolint::config::HadolintConfig;
61
62    fn lint_dockerfile(content: &str) -> LintResult {
63        lint(content, &HadolintConfig::default())
64    }
65
66    #[test]
67    fn test_copy_from_same_stage() {
68        let result = lint_dockerfile(
69            "FROM node:18 AS builder\nCOPY --from=builder /app /app"
70        );
71        assert!(result.failures.iter().any(|f| f.code.as_str() == "DL3023"));
72    }
73
74    #[test]
75    fn test_copy_from_same_index() {
76        let result = lint_dockerfile(
77            "FROM node:18\nCOPY --from=0 /app /app"
78        );
79        assert!(result.failures.iter().any(|f| f.code.as_str() == "DL3023"));
80    }
81
82    #[test]
83    fn test_copy_from_different_stage() {
84        let result = lint_dockerfile(
85            "FROM node:18 AS builder\nRUN npm ci\nFROM node:18-alpine\nCOPY --from=builder /app /app"
86        );
87        assert!(!result.failures.iter().any(|f| f.code.as_str() == "DL3023"));
88    }
89
90    #[test]
91    fn test_copy_without_from() {
92        let result = lint_dockerfile("FROM node:18 AS builder\nCOPY package.json /app/");
93        assert!(!result.failures.iter().any(|f| f.code.as_str() == "DL3023"));
94    }
95}