syncable_cli/analyzer/hadolint/rules/
dl3008.rs

1//! DL3008: Pin versions in apt-get install
2//!
3//! Package versions should be pinned in apt-get install to ensure
4//! reproducible builds.
5
6use 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                        // Get apt-get install packages
21                        let packages = apt_get_packages(shell);
22                        // All packages should have versions pinned
23                        packages.iter().all(|pkg| is_version_pinned(pkg))
24                    } else {
25                        true
26                    }
27                }
28                _ => true,
29            }
30        },
31    )
32}
33
34/// Extract packages from apt-get install commands.
35fn 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            // Get arguments that aren't flags and aren't "install"
41            let args: Vec<&str> = cmd
42                .args_no_flags()
43                .into_iter()
44                .filter(|a| *a != "install")
45                // Filter out -t/--target-release arguments
46                .collect();
47
48            packages.extend(args.into_iter().map(|s| s.to_string()));
49        }
50    }
51
52    packages
53}
54
55/// Check if a package has a version pinned.
56fn is_version_pinned(package: &str) -> bool {
57    // Version pinned: package=version
58    package.contains('=')
59        // APT pinning: package/release
60        || package.contains('/')
61        // Local .deb file
62        || 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}