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    ];
22
23    let mut findings = Vec::new();
24    for rule in &rules {
25        findings.extend(rule.check(config));
26    }
27    findings
28}
29
30#[cfg(test)]
31mod tests {
32    use super::*;
33    use crate::model::{Config, Finding, Item, Severity, Span};
34
35    struct DummyRule;
36    impl Rule for DummyRule {
37        fn name(&self) -> &'static str {
38            "dummy"
39        }
40        fn check(&self, _config: &Config) -> Vec<Finding> {
41            vec![Finding::new(
42                Severity::Info,
43                "dummy",
44                "TEST",
45                "this is a test",
46                Span::new(1),
47            )]
48        }
49    }
50
51    #[test]
52    fn trait_rule_returns_finding() {
53        let config = Config { items: vec![] };
54        let rule = DummyRule;
55        let findings = rule.check(&config);
56        assert_eq!(findings.len(), 1);
57        assert_eq!(findings[0].rule, "dummy");
58    }
59
60    #[test]
61    fn run_all_merges_findings() {
62        // run_all on an empty config should return no errors
63        let config = Config { items: vec![] };
64        let findings = run_all(&config);
65        // All rules on empty config should produce no findings
66        assert!(findings.is_empty());
67    }
68
69    #[test]
70    fn run_all_on_config_with_duplicates() {
71        let config = Config {
72            items: vec![
73                Item::HostBlock {
74                    patterns: vec!["github.com".to_string()],
75                    span: Span::new(1),
76                    items: vec![],
77                },
78                Item::HostBlock {
79                    patterns: vec!["github.com".to_string()],
80                    span: Span::new(5),
81                    items: vec![],
82                },
83            ],
84        };
85        let findings = run_all(&config);
86        assert!(findings.iter().any(|f| f.rule == "duplicate-host"));
87    }
88}