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