syncable_cli/analyzer/hadolint/rules/
dl3062.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 "DL3062",
14 Severity::Warning,
15 "`COPY --from` should reference a defined build stage or an external image.",
16 |state, line, instr, _shell| {
17 match instr {
18 Instruction::From(base_image) => {
19 if let Some(alias) = &base_image.alias {
21 state
22 .data
23 .insert_to_set("stages", alias.as_str().to_string());
24 }
25 let count = state.data.get_int("stage_count");
27 state.data.insert_to_set("stages", count.to_string());
28 state.data.set_int("stage_count", count + 1);
29 }
30 Instruction::Copy(_, flags) => {
31 if let Some(from) = &flags.from {
32 let from_str = from.as_str();
33
34 let is_stage_alias = state.data.set_contains("stages", from_str);
40 let is_stage_index = from_str.parse::<usize>().is_ok();
41 let is_external = from_str.contains('/')
42 || from_str.contains('.')
43 || from_str.contains(':');
44
45 if !is_stage_alias && !is_stage_index && !is_external {
46 state.add_failure("DL3062", Severity::Warning, "`COPY --from` should reference a defined build stage or an external image.", line);
47 }
48 }
49 }
50 _ => {}
51 }
52 },
53 )
54}
55
56#[cfg(test)]
57mod tests {
58 use super::*;
59 use crate::analyzer::hadolint::config::HadolintConfig;
60 use crate::analyzer::hadolint::lint::{LintResult, lint};
61
62 fn lint_dockerfile(content: &str) -> LintResult {
63 lint(content, &HadolintConfig::default())
64 }
65
66 #[test]
67 fn test_copy_from_defined_stage() {
68 let result = lint_dockerfile(
69 "FROM ubuntu:20.04 AS builder\nRUN echo hello\nFROM alpine:3.14\nCOPY --from=builder /app /app",
70 );
71 assert!(!result.failures.iter().any(|f| f.code.as_str() == "DL3062"));
72 }
73
74 #[test]
75 fn test_copy_from_stage_index() {
76 let result = lint_dockerfile(
77 "FROM ubuntu:20.04\nRUN echo hello\nFROM alpine:3.14\nCOPY --from=0 /app /app",
78 );
79 assert!(!result.failures.iter().any(|f| f.code.as_str() == "DL3062"));
80 }
81
82 #[test]
83 fn test_copy_from_external_image() {
84 let result =
85 lint_dockerfile("FROM ubuntu:20.04\nCOPY --from=nginx:latest /etc/nginx /etc/nginx");
86 assert!(!result.failures.iter().any(|f| f.code.as_str() == "DL3062"));
87 }
88
89 #[test]
90 fn test_copy_from_undefined_stage() {
91 let result = lint_dockerfile("FROM ubuntu:20.04\nCOPY --from=nonexistent /app /app");
92 assert!(result.failures.iter().any(|f| f.code.as_str() == "DL3062"));
93 }
94}