syncable_cli/analyzer/hadolint/rules/
dl3023.rs1use 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 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 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 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}