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