firewall_objects/service/application/
mod.rs

1//! Application object definitions and lightweight match helpers.
2//!
3//! This module exposes the types used to describe Layer-7 applications and
4//! provides simple matching primitives. Sample application entries live in the
5//! [`catalog`] submodule so consumers can replace or extend them as needed.
6//!
7//! Feature flags:
8//! - `serde` – Enables serialization for the public structs.
9
10pub mod catalog;
11
12use crate::service::TransportService;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub struct ApplicationIndicators<'a> {
16    pub dns_suffixes: &'a [&'a str],
17    pub tls_sni_suffixes: &'a [&'a str],
18    pub http_hosts: &'a [&'a str],
19}
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub struct ApplicationDefinition<'a> {
23    pub name: &'a str,
24    pub category: &'a str,
25    pub transports: &'a [TransportService],
26    pub indicators: ApplicationIndicators<'a>,
27}
28
29#[derive(Default)]
30pub struct ApplicationMatchInput<'a> {
31    pub dns_query: Option<&'a str>,
32    pub tls_sni: Option<&'a str>,
33    pub http_host: Option<&'a str>,
34}
35
36/// Owned application object suitable for serialization and CRUD operations.
37#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
38#[derive(Debug, Clone, PartialEq)]
39pub struct ApplicationObj {
40    pub name: String,
41    pub category: String,
42    pub transports: Vec<TransportService>,
43    pub dns_suffixes: Vec<String>,
44    pub tls_sni_suffixes: Vec<String>,
45    pub http_hosts: Vec<String>,
46}
47
48impl<'a> ApplicationDefinition<'a> {
49    /// Returns true when the provided metadata satisfies this application's indicators.
50    ///
51    /// ```
52    /// use firewall_objects::service::application::{
53    ///     ApplicationDefinition, ApplicationIndicators, ApplicationMatchInput,
54    /// };
55    /// use firewall_objects::service::TransportService;
56    ///
57    /// let app = ApplicationDefinition {
58    ///     name: "example",
59    ///     category: "test",
60    ///     transports: &[TransportService::tcp(443)],
61    ///     indicators: ApplicationIndicators {
62    ///         dns_suffixes: &[".example.com"],
63    ///         tls_sni_suffixes: &[],
64    ///         http_hosts: &["example.com"],
65    ///     },
66    /// };
67    ///
68    /// let input = ApplicationMatchInput {
69    ///     http_host: Some("example.com"),
70    ///     ..Default::default()
71    /// };
72    ///
73    /// assert!(app.matches(&input));
74    /// ```
75    pub fn matches(&self, input: &ApplicationMatchInput<'_>) -> bool {
76        self.indicators.matches(input)
77    }
78}
79
80impl<'a> ApplicationIndicators<'a> {
81    pub fn matches(&self, input: &ApplicationMatchInput<'_>) -> bool {
82        if let Some(query) = input.dns_query {
83            if self
84                .dns_suffixes
85                .iter()
86                .any(|suffix| query.to_ascii_lowercase().ends_with(suffix))
87            {
88                return true;
89            }
90        }
91
92        if let Some(sni) = input.tls_sni {
93            if self
94                .tls_sni_suffixes
95                .iter()
96                .any(|suffix| sni.to_ascii_lowercase().ends_with(suffix))
97            {
98                return true;
99            }
100        }
101
102        if let Some(host) = input.http_host {
103            if self
104                .http_hosts
105                .iter()
106                .any(|expected| host.eq_ignore_ascii_case(expected))
107            {
108                return true;
109            }
110        }
111
112        false
113    }
114}
115
116impl ApplicationObj {
117    /// Evaluate indicators against observed metadata.
118    pub fn matches(&self, input: &ApplicationMatchInput<'_>) -> bool {
119        let dns_query = input.dns_query.map(|v| v.to_ascii_lowercase());
120        if let Some(ref query) = dns_query {
121            if self
122                .dns_suffixes
123                .iter()
124                .any(|suffix| query.ends_with(suffix))
125            {
126                return true;
127            }
128        }
129
130        let tls_sni = input.tls_sni.map(|v| v.to_ascii_lowercase());
131        if let Some(ref sni) = tls_sni {
132            if self
133                .tls_sni_suffixes
134                .iter()
135                .any(|suffix| sni.ends_with(suffix))
136            {
137                return true;
138            }
139        }
140
141        if let Some(host) = input.http_host {
142            if self
143                .http_hosts
144                .iter()
145                .any(|expected| host.eq_ignore_ascii_case(expected))
146            {
147                return true;
148            }
149        }
150
151        false
152    }
153}
154
155impl<'a> From<ApplicationDefinition<'a>> for ApplicationObj {
156    fn from(def: ApplicationDefinition<'a>) -> Self {
157        Self {
158            name: def.name.to_string(),
159            category: def.category.to_string(),
160            transports: def.transports.to_vec(),
161            dns_suffixes: def
162                .indicators
163                .dns_suffixes
164                .iter()
165                .map(|s| s.to_string())
166                .collect(),
167            tls_sni_suffixes: def
168                .indicators
169                .tls_sni_suffixes
170                .iter()
171                .map(|s| s.to_string())
172                .collect(),
173            http_hosts: def
174                .indicators
175                .http_hosts
176                .iter()
177                .map(|s| s.to_string())
178                .collect(),
179        }
180    }
181}
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186
187    #[test]
188    fn matches_dns_and_http() {
189        let app = ApplicationDefinition {
190            name: "demo",
191            category: "qa",
192            transports: &[TransportService::tcp(443)],
193            indicators: ApplicationIndicators {
194                dns_suffixes: &[".demo.local"],
195                tls_sni_suffixes: &[".demo.local"],
196                http_hosts: &["demo.local"],
197            },
198        };
199
200        let dns = ApplicationMatchInput {
201            dns_query: Some("api.demo.local"),
202            ..Default::default()
203        };
204        assert!(app.matches(&dns));
205
206        let http = ApplicationMatchInput {
207            http_host: Some("Demo.Local"),
208            ..Default::default()
209        };
210        assert!(app.matches(&http));
211    }
212
213    #[test]
214    fn owned_application_matches_metadata() {
215        let def = ApplicationDefinition {
216            name: "owned",
217            category: "sample",
218            transports: &[TransportService::tcp(8443)],
219            indicators: ApplicationIndicators {
220                dns_suffixes: &[".owned.local"],
221                tls_sni_suffixes: &[],
222                http_hosts: &["owned.local"],
223            },
224        };
225        let obj: ApplicationObj = def.into();
226        let input = ApplicationMatchInput {
227            http_host: Some("owned.local"),
228            ..Default::default()
229        };
230        assert!(obj.matches(&input));
231    }
232}