syncable_cli/analyzer/hadolint/rules/
dl3045.rs1use crate::analyzer::hadolint::parser::instruction::Instruction;
7use crate::analyzer::hadolint::rules::{CustomRule, RuleState, custom_rule};
8use crate::analyzer::hadolint::shell::ParsedShell;
9use crate::analyzer::hadolint::types::Severity;
10
11pub fn rule()
12-> CustomRule<impl Fn(&mut RuleState, u32, &Instruction, Option<&ParsedShell>) + Send + Sync> {
13 custom_rule(
14 "DL3045",
15 Severity::Warning,
16 "`COPY` to a relative destination without `WORKDIR` set.",
17 |state, line, instr, _shell| {
18 match instr {
19 Instruction::From(base) => {
20 let stage_name = base
22 .alias
23 .as_ref()
24 .map(|a| a.as_str().to_string())
25 .unwrap_or_else(|| base.image.name.clone());
26 state.data.set_string("current_stage", &stage_name);
27
28 let parent_had_workdir = state
30 .data
31 .set_contains("stages_with_workdir", &base.image.name);
32 if parent_had_workdir {
33 state.data.insert_to_set("stages_with_workdir", &stage_name);
34 }
35 }
36 Instruction::Workdir(_) => {
37 let stage = state
39 .data
40 .get_string("current_stage")
41 .map(|s| s.to_string())
42 .unwrap_or_else(|| "__none__".to_string());
43 state.data.insert_to_set("stages_with_workdir", &stage);
44 }
45 Instruction::Copy(args, _) => {
46 let dest = &args.dest;
47
48 let has_workdir = state
50 .data
51 .get_string("current_stage")
52 .map(|s| state.data.set_contains("stages_with_workdir", s))
53 .unwrap_or_else(|| {
54 state.data.set_contains("stages_with_workdir", "__none__")
55 });
56
57 if has_workdir {
59 return;
60 }
61
62 let trimmed = dest.trim_matches(|c| c == '"' || c == '\'');
64
65 if trimmed.starts_with('/') {
67 return;
68 }
69
70 if is_windows_absolute(trimmed) {
72 return;
73 }
74
75 if trimmed.starts_with('$') {
77 return;
78 }
79
80 state.add_failure(
82 "DL3045",
83 Severity::Warning,
84 "`COPY` to a relative destination without `WORKDIR` set.",
85 line,
86 );
87 }
88 _ => {}
89 }
90 },
91 )
92}
93
94fn is_windows_absolute(path: &str) -> bool {
96 let chars: Vec<char> = path.chars().collect();
97 chars.len() >= 2 && chars[0].is_ascii_alphabetic() && chars[1] == ':'
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103 use crate::analyzer::hadolint::parser::instruction::{BaseImage, CopyArgs, CopyFlags};
104 use crate::analyzer::hadolint::rules::Rule;
105
106 #[test]
107 fn test_absolute_dest() {
108 let rule = rule();
109 let mut state = RuleState::new();
110
111 let from = Instruction::From(BaseImage::new("ubuntu"));
112 let copy = Instruction::Copy(
113 CopyArgs::new(vec!["app.js".to_string()], "/app/"),
114 CopyFlags::default(),
115 );
116
117 rule.check(&mut state, 1, &from, None);
118 rule.check(&mut state, 2, ©, None);
119 assert!(state.failures.is_empty());
120 }
121
122 #[test]
123 fn test_relative_dest_without_workdir() {
124 let rule = rule();
125 let mut state = RuleState::new();
126
127 let from = Instruction::From(BaseImage::new("ubuntu"));
128 let copy = Instruction::Copy(
129 CopyArgs::new(vec!["app.js".to_string()], "app/"),
130 CopyFlags::default(),
131 );
132
133 rule.check(&mut state, 1, &from, None);
134 rule.check(&mut state, 2, ©, None);
135 assert_eq!(state.failures.len(), 1);
136 assert_eq!(state.failures[0].code.as_str(), "DL3045");
137 }
138
139 #[test]
140 fn test_relative_dest_with_workdir() {
141 let rule = rule();
142 let mut state = RuleState::new();
143
144 let from = Instruction::From(BaseImage::new("ubuntu"));
145 let workdir = Instruction::Workdir("/app".to_string());
146 let copy = Instruction::Copy(
147 CopyArgs::new(vec!["app.js".to_string()], "."),
148 CopyFlags::default(),
149 );
150
151 rule.check(&mut state, 1, &from, None);
152 rule.check(&mut state, 2, &workdir, None);
153 rule.check(&mut state, 3, ©, None);
154 assert!(state.failures.is_empty());
155 }
156
157 #[test]
158 fn test_variable_dest() {
159 let rule = rule();
160 let mut state = RuleState::new();
161
162 let from = Instruction::From(BaseImage::new("ubuntu"));
163 let copy = Instruction::Copy(
164 CopyArgs::new(vec!["app.js".to_string()], "$APP_DIR"),
165 CopyFlags::default(),
166 );
167
168 rule.check(&mut state, 1, &from, None);
169 rule.check(&mut state, 2, ©, None);
170 assert!(state.failures.is_empty());
171 }
172}