syncable_cli/analyzer/hadolint/rules/
dl3052.rs

1//! DL3052: Label `org.opencontainers.image.licenses` is not a valid SPDX expression
2//!
3//! The licenses label should contain a valid SPDX license identifier.
4
5use crate::analyzer::hadolint::parser::instruction::Instruction;
6use crate::analyzer::hadolint::rules::{simple_rule, SimpleRule};
7use crate::analyzer::hadolint::shell::ParsedShell;
8use crate::analyzer::hadolint::types::Severity;
9
10pub fn rule() -> SimpleRule<impl Fn(&Instruction, Option<&ParsedShell>) -> bool + Send + Sync> {
11    simple_rule(
12        "DL3052",
13        Severity::Warning,
14        "Label `org.opencontainers.image.licenses` is not a valid SPDX expression.",
15        |instr, _shell| {
16            match instr {
17                Instruction::Label(pairs) => {
18                    for (key, value) in pairs {
19                        if key == "org.opencontainers.image.licenses" {
20                            if value.is_empty() || !is_valid_spdx(value) {
21                                return false;
22                            }
23                        }
24                    }
25                    true
26                }
27                _ => true,
28            }
29        },
30    )
31}
32
33fn is_valid_spdx(license: &str) -> bool {
34    // Common SPDX license identifiers
35    let common_licenses = [
36        "MIT", "Apache-2.0", "GPL-2.0", "GPL-2.0-only", "GPL-2.0-or-later",
37        "GPL-3.0", "GPL-3.0-only", "GPL-3.0-or-later", "BSD-2-Clause",
38        "BSD-3-Clause", "ISC", "MPL-2.0", "LGPL-2.1", "LGPL-2.1-only",
39        "LGPL-2.1-or-later", "LGPL-3.0", "LGPL-3.0-only", "LGPL-3.0-or-later",
40        "AGPL-3.0", "AGPL-3.0-only", "AGPL-3.0-or-later", "Unlicense",
41        "CC0-1.0", "CC-BY-4.0", "CC-BY-SA-4.0", "WTFPL", "Zlib", "0BSD",
42        "EPL-1.0", "EPL-2.0", "EUPL-1.2", "PostgreSQL", "OFL-1.1",
43        "Artistic-2.0", "BSL-1.0", "CDDL-1.0", "CDDL-1.1", "CPL-1.0",
44    ];
45
46    // Check for common licenses (case-insensitive)
47    let license_upper = license.to_uppercase();
48
49    // Handle compound expressions (AND, OR, WITH)
50    let parts: Vec<&str> = license_upper
51        .split(|c| c == '(' || c == ')' || c == ' ')
52        .filter(|s| !s.is_empty() && *s != "AND" && *s != "OR" && *s != "WITH")
53        .collect();
54
55    if parts.is_empty() {
56        return false;
57    }
58
59    parts.iter().all(|part| {
60        common_licenses.iter().any(|l| l.to_uppercase() == *part)
61    })
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67    use crate::analyzer::hadolint::lint::{lint, LintResult};
68    use crate::analyzer::hadolint::config::HadolintConfig;
69
70    fn lint_dockerfile(content: &str) -> LintResult {
71        lint(content, &HadolintConfig::default())
72    }
73
74    #[test]
75    fn test_valid_spdx() {
76        let result = lint_dockerfile("FROM ubuntu:20.04\nLABEL org.opencontainers.image.licenses=\"MIT\"");
77        assert!(!result.failures.iter().any(|f| f.code.as_str() == "DL3052"));
78    }
79
80    #[test]
81    fn test_valid_compound_spdx() {
82        let result = lint_dockerfile("FROM ubuntu:20.04\nLABEL org.opencontainers.image.licenses=\"MIT OR Apache-2.0\"");
83        assert!(!result.failures.iter().any(|f| f.code.as_str() == "DL3052"));
84    }
85
86    #[test]
87    fn test_invalid_spdx() {
88        let result = lint_dockerfile("FROM ubuntu:20.04\nLABEL org.opencontainers.image.licenses=\"NotALicense\"");
89        assert!(result.failures.iter().any(|f| f.code.as_str() == "DL3052"));
90    }
91}