huginn_net_db/
observable_http_signals_matching.rs

1use crate::db::HttpIndexKey;
2use crate::db_matching_trait::{DatabaseSignature, MatchQuality};
3use crate::http::{self, Header, HttpMatchQuality, Version};
4use crate::observable_signals::{HttpRequestObservation, HttpResponseObservation};
5
6pub trait HttpDistance {
7    fn get_version(&self) -> Version;
8    fn get_horder(&self) -> &[Header];
9    fn get_habsent(&self) -> &[Header];
10    fn get_expsw(&self) -> &str;
11
12    fn distance_ip_version(&self, other: &http::Signature) -> Option<u32> {
13        if other.version == Version::Any || self.get_version() == other.version {
14            Some(HttpMatchQuality::High.as_score())
15        } else {
16            None
17        }
18    }
19
20    // Compare two header vectors respecting order and allowing optional header skips
21    //
22    // This function implements a sophisticated two-pointer algorithm to compare HTTP headers
23    // from observed traffic against database signatures while preserving order and handling
24    // optional headers that may be missing from the observed traffic.
25    //
26    // Algorithm Overview:
27    // 1. Use two pointers to traverse both lists simultaneously
28    // 2. When headers match perfectly (name + value), advance both pointers
29    // 3. When names match but values differ, count as error only if header is required
30    // 4. When names differ, skip optional signature headers or count required ones as errors
31    // 5. Handle remaining headers at the end of either list
32    //
33    // Parameters:
34    // - observed: Headers from actual HTTP traffic (never marked as optional)
35    // - signature: Headers from database signature (may have optional headers marked with ?)
36    //
37    // Returns:
38    // - Some(score) based on error count converted to quality score
39    // - None if too many errors (unmatchable)
40    fn distance_header(observed: &[Header], signature: &[Header]) -> Option<u32> {
41        let mut obs_idx = 0usize; // Index pointer for observed headers
42        let mut sig_idx = 0usize; // Index pointer for signature headers
43        let mut errors: u32 = 0; // Running count of matching errors
44
45        while obs_idx < observed.len() && sig_idx < signature.len() {
46            let obs_header = &observed[obs_idx];
47            let sig_header = &signature[sig_idx];
48
49            if obs_header.name == sig_header.name && obs_header.value == sig_header.value {
50                obs_idx = obs_idx.saturating_add(1);
51                sig_idx = sig_idx.saturating_add(1);
52            } else if obs_header.name == sig_header.name {
53                if !sig_header.optional {
54                    errors = errors.saturating_add(1);
55                }
56                obs_idx = obs_idx.saturating_add(1);
57                sig_idx = sig_idx.saturating_add(1);
58            } else if sig_header.optional {
59                sig_idx = sig_idx.saturating_add(1);
60            } else {
61                errors = errors.saturating_add(1);
62                sig_idx = sig_idx.saturating_add(1);
63            }
64        }
65
66        while obs_idx < observed.len() {
67            errors = errors.saturating_add(1);
68            obs_idx = obs_idx.saturating_add(1);
69        }
70
71        while sig_idx < signature.len() {
72            if !signature[sig_idx].optional {
73                errors = errors.saturating_add(1);
74            }
75            sig_idx = sig_idx.saturating_add(1);
76        }
77
78        match errors {
79            0..=2 => Some(HttpMatchQuality::High.as_score()), // 0-2 errors: High quality match
80            3..=5 => Some(HttpMatchQuality::Medium.as_score()), // 3-5 errors: Medium quality match
81            6..=8 => Some(HttpMatchQuality::Low.as_score()),  // 6-8 errors: Low quality match
82            9..=11 => Some(HttpMatchQuality::Bad.as_score()), // 9-11 errors: Bad quality match
83            _ => None, // 12+ errors: Too many differences, not a viable match
84        }
85    }
86
87    fn distance_horder(&self, other: &http::Signature) -> Option<u32> {
88        Self::distance_header(self.get_horder(), &other.horder)
89    }
90
91    fn distance_habsent(&self, other: &http::Signature) -> Option<u32> {
92        Self::distance_header(self.get_habsent(), &other.habsent)
93    }
94
95    fn distance_expsw(&self, other: &http::Signature) -> Option<u32> {
96        if other.expsw.as_str().contains(self.get_expsw()) {
97            Some(HttpMatchQuality::High.as_score())
98        } else {
99            Some(HttpMatchQuality::Bad.as_score())
100        }
101    }
102}
103
104impl HttpDistance for HttpRequestObservation {
105    fn get_version(&self) -> Version {
106        self.version
107    }
108    fn get_horder(&self) -> &[Header] {
109        &self.horder
110    }
111    fn get_habsent(&self) -> &[Header] {
112        &self.habsent
113    }
114    fn get_expsw(&self) -> &str {
115        &self.expsw
116    }
117}
118
119impl HttpDistance for HttpResponseObservation {
120    fn get_version(&self) -> Version {
121        self.version
122    }
123    fn get_horder(&self) -> &[Header] {
124        &self.horder
125    }
126    fn get_habsent(&self) -> &[Header] {
127        &self.habsent
128    }
129    fn get_expsw(&self) -> &str {
130        &self.expsw
131    }
132}
133
134trait HttpSignatureHelper {
135    fn calculate_http_distance<T: HttpDistance>(&self, observed: &T) -> Option<u32>;
136
137    fn generate_http_index_keys(&self) -> Vec<HttpIndexKey>;
138
139    /// Returns the quality score based on the distance.
140    ///
141    /// The score is a value between 0.0 and 1.0, where 1.0 is a perfect match.
142    ///
143    /// The score is calculated based on the distance of the observed signal to the database signature.
144    /// The distance is a value between 0 and 12, where 0 is a perfect match and 12 is the maximum possible distance.
145    fn get_quality_score_by_distance(&self, distance: u32) -> f32 {
146        http::HttpMatchQuality::distance_to_score(distance)
147    }
148}
149
150impl HttpSignatureHelper for http::Signature {
151    fn calculate_http_distance<T: HttpDistance>(&self, observed: &T) -> Option<u32> {
152        let signature: &http::Signature = self;
153        let distance = observed
154            .distance_ip_version(signature)?
155            .saturating_add(observed.distance_horder(signature)?)
156            .saturating_add(observed.distance_habsent(signature)?)
157            .saturating_add(observed.distance_expsw(signature)?);
158        Some(distance)
159    }
160    fn generate_http_index_keys(&self) -> Vec<HttpIndexKey> {
161        let mut keys = Vec::new();
162        if self.version == Version::Any {
163            keys.push(HttpIndexKey {
164                http_version_key: Version::V10,
165            });
166            keys.push(HttpIndexKey {
167                http_version_key: Version::V11,
168            });
169        } else {
170            keys.push(HttpIndexKey {
171                http_version_key: self.version,
172            });
173        }
174        keys
175    }
176}
177
178impl DatabaseSignature<HttpRequestObservation> for http::Signature {
179    fn calculate_distance(&self, observed: &HttpRequestObservation) -> Option<u32> {
180        self.calculate_http_distance(observed)
181    }
182    fn get_quality_score(&self, distance: u32) -> f32 {
183        self.get_quality_score_by_distance(distance)
184    }
185    fn generate_index_keys_for_db_entry(&self) -> Vec<HttpIndexKey> {
186        self.generate_http_index_keys()
187    }
188}
189
190impl DatabaseSignature<HttpResponseObservation> for http::Signature {
191    fn calculate_distance(&self, observed: &HttpResponseObservation) -> Option<u32> {
192        self.calculate_http_distance(observed)
193    }
194    fn get_quality_score(&self, distance: u32) -> f32 {
195        self.get_quality_score_by_distance(distance)
196    }
197    fn generate_index_keys_for_db_entry(&self) -> Vec<HttpIndexKey> {
198        self.generate_http_index_keys()
199    }
200}