syncable_cli/analyzer/hadolint/rules/
dl3008.rs1use crate::analyzer::hadolint::parser::instruction::Instruction;
7use crate::analyzer::hadolint::rules::{SimpleRule, simple_rule};
8use crate::analyzer::hadolint::shell::ParsedShell;
9use crate::analyzer::hadolint::types::Severity;
10
11pub fn rule() -> SimpleRule<impl Fn(&Instruction, Option<&ParsedShell>) -> bool + Send + Sync> {
12 simple_rule(
13 "DL3008",
14 Severity::Warning,
15 "Pin versions in apt get install. Instead of `apt-get install <package>` use `apt-get install <package>=<version>`",
16 |instr, shell| {
17 match instr {
18 Instruction::Run(_) => {
19 if let Some(shell) = shell {
20 let packages = apt_get_packages(shell);
22 packages.iter().all(|pkg| is_version_pinned(pkg))
24 } else {
25 true
26 }
27 }
28 _ => true,
29 }
30 },
31 )
32}
33
34fn apt_get_packages(shell: &ParsedShell) -> Vec<String> {
36 let mut packages = Vec::new();
37
38 for cmd in &shell.commands {
39 if cmd.name == "apt-get" && cmd.arguments.iter().any(|a| a == "install") {
40 let args: Vec<&str> = cmd
42 .args_no_flags()
43 .into_iter()
44 .filter(|a| *a != "install")
45 .collect();
47
48 packages.extend(args.into_iter().map(|s| s.to_string()));
49 }
50 }
51
52 packages
53}
54
55fn is_version_pinned(package: &str) -> bool {
57 package.contains('=')
59 || package.contains('/')
61 || package.ends_with(".deb")
63}
64
65#[cfg(test)]
66mod tests {
67 use super::*;
68 use crate::analyzer::hadolint::parser::instruction::RunArgs;
69 use crate::analyzer::hadolint::rules::{Rule, RuleState};
70
71 #[test]
72 fn test_pinned_version() {
73 let rule = rule();
74 let mut state = RuleState::new();
75
76 let instr = Instruction::Run(RunArgs::shell("apt-get install -y nginx=1.18.0-0ubuntu1"));
77 let shell = ParsedShell::parse("apt-get install -y nginx=1.18.0-0ubuntu1");
78 rule.check(&mut state, 1, &instr, Some(&shell));
79 assert!(state.failures.is_empty());
80 }
81
82 #[test]
83 fn test_unpinned_version() {
84 let rule = rule();
85 let mut state = RuleState::new();
86
87 let instr = Instruction::Run(RunArgs::shell("apt-get install -y nginx"));
88 let shell = ParsedShell::parse("apt-get install -y nginx");
89 rule.check(&mut state, 1, &instr, Some(&shell));
90 assert_eq!(state.failures.len(), 1);
91 assert_eq!(state.failures[0].code.as_str(), "DL3008");
92 }
93
94 #[test]
95 fn test_apt_pinning() {
96 let rule = rule();
97 let mut state = RuleState::new();
98
99 let instr = Instruction::Run(RunArgs::shell("apt-get install -y nginx/focal"));
100 let shell = ParsedShell::parse("apt-get install -y nginx/focal");
101 rule.check(&mut state, 1, &instr, Some(&shell));
102 assert!(state.failures.is_empty());
103 }
104
105 #[test]
106 fn test_update_only() {
107 let rule = rule();
108 let mut state = RuleState::new();
109
110 let instr = Instruction::Run(RunArgs::shell("apt-get update"));
111 let shell = ParsedShell::parse("apt-get update");
112 rule.check(&mut state, 1, &instr, Some(&shell));
113 assert!(state.failures.is_empty());
114 }
115}