Skip to main content

sshconfig_lint/rules/
mod.rs

1pub mod basic;
2
3use crate::model::{Config, Finding};
4
5/// A lint rule that checks a parsed config.
6pub trait Rule {
7    /// Human-readable name for this rule.
8    fn name(&self) -> &'static str;
9    /// Check the config and return any findings.
10    fn check(&self, config: &Config) -> Vec<Finding>;
11}
12
13/// Run all registered rules against a config and return merged findings.
14pub fn run_all(config: &Config) -> Vec<Finding> {
15    let rules: Vec<Box<dyn Rule>> = vec![
16        Box::new(basic::DuplicateHost),
17        Box::new(basic::IdentityFileExists),
18        Box::new(basic::WildcardHostOrder),
19        Box::new(basic::DeprecatedWeakAlgorithms),
20        Box::new(basic::DuplicateDirectives),
21        Box::new(basic::InsecureOption),
22        Box::new(basic::UnsafeControlPath),
23    ];
24
25    let mut findings = Vec::new();
26    for rule in &rules {
27        findings.extend(rule.check(config));
28    }
29    findings
30}
31
32#[cfg(test)]
33mod tests {
34    use super::*;
35    use crate::model::{Config, Finding, Item, Severity, Span};
36
37    struct DummyRule;
38    impl Rule for DummyRule {
39        fn name(&self) -> &'static str {
40            "dummy"
41        }
42        fn check(&self, _config: &Config) -> Vec<Finding> {
43            vec![Finding::new(
44                Severity::Info,
45                "dummy",
46                "TEST",
47                "this is a test",
48                Span::new(1),
49            )]
50        }
51    }
52
53    #[test]
54    fn trait_rule_returns_finding() {
55        let config = Config { items: vec![] };
56        let rule = DummyRule;
57        let findings = rule.check(&config);
58        assert_eq!(findings.len(), 1);
59        assert_eq!(findings[0].rule, "dummy");
60    }
61
62    #[test]
63    fn run_all_merges_findings() {
64        // run_all on an empty config should return no errors
65        let config = Config { items: vec![] };
66        let findings = run_all(&config);
67        // All rules on empty config should produce no findings
68        assert!(findings.is_empty());
69    }
70
71    #[test]
72    fn run_all_on_config_with_duplicates() {
73        let config = Config {
74            items: vec![
75                Item::HostBlock {
76                    patterns: vec!["github.com".to_string()],
77                    span: Span::new(1),
78                    items: vec![],
79                },
80                Item::HostBlock {
81                    patterns: vec!["github.com".to_string()],
82                    span: Span::new(5),
83                    items: vec![],
84                },
85            ],
86        };
87        let findings = run_all(&config);
88        assert!(findings.iter().any(|f| f.rule == "duplicate-host"));
89    }
90}