gossan_classify/
classifier.rs1use crate::matcher::CpuMatcher;
9use crate::rules::{builtin_rules, ServiceMatch, ServiceRule};
10
11pub struct BannerClassifier {
14 matcher: CpuMatcher,
15}
16
17impl BannerClassifier {
18 #[must_use]
20 pub fn new() -> Self {
21 Self {
22 matcher: CpuMatcher::new(builtin_rules()),
23 }
24 }
25
26 #[must_use]
29 pub fn with_rules(rules: Vec<ServiceRule>) -> Self {
30 Self {
31 matcher: CpuMatcher::new(rules),
32 }
33 }
34
35 #[must_use]
38 pub fn classify(&self, banner: &str) -> Vec<ServiceMatch> {
39 self.matcher.match_banner(banner)
40 }
41
42 #[must_use]
46 pub fn classify_batch(&self, banners: &[&str]) -> Vec<Vec<ServiceMatch>> {
47 self.matcher.match_batch(banners)
48 }
49
50 #[must_use]
54 pub fn classify_top(&self, banner: &str) -> Option<ServiceMatch> {
55 self.matcher.match_banner(banner).into_iter().next()
56 }
57}
58
59impl Default for BannerClassifier {
60 fn default() -> Self {
61 Self::new()
62 }
63}
64
65#[cfg(test)]
66mod tests {
67 use super::*;
68
69 #[test]
70 fn classifier_loads_builtin_rules_without_panic() {
71 let c = BannerClassifier::new();
72 let _ = c.classify("Server: nginx/1.25.3\r\n");
73 }
74
75 #[test]
76 fn classify_top_returns_none_for_garbage() {
77 let c = BannerClassifier::new();
78 assert!(c.classify_top("\x00\x00\x00\x00").is_none());
79 }
80
81 #[test]
82 fn classify_batch_preserves_ordering() {
83 let c = BannerClassifier::new();
84 let banners = ["Server: nginx", "SSH-2.0-OpenSSH_8.9", "garbage"];
85 let out = c.classify_batch(&banners);
86 assert_eq!(out.len(), banners.len(), "one result vec per input banner");
87 }
88
89 #[test]
90 fn with_rules_uses_caller_rule_set() {
91 let c = BannerClassifier::with_rules(vec![]);
94 assert!(c.classify("Server: nginx/1.25.3").is_empty());
95 assert!(c.classify_top("anything").is_none());
96 }
97}