Skip to main content

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            && self
84                .dns_suffixes
85                .iter()
86                .any(|suffix| query.to_ascii_lowercase().ends_with(suffix))
87        {
88            return true;
89        }
90
91        if let Some(sni) = input.tls_sni
92            && self
93                .tls_sni_suffixes
94                .iter()
95                .any(|suffix| sni.to_ascii_lowercase().ends_with(suffix))
96        {
97            return true;
98        }
99
100        if let Some(host) = input.http_host
101            && self
102                .http_hosts
103                .iter()
104                .any(|expected| host.eq_ignore_ascii_case(expected))
105        {
106            return true;
107        }
108
109        false
110    }
111}
112
113impl ApplicationObj {
114    /// Evaluate indicators against observed metadata.
115    pub fn matches(&self, input: &ApplicationMatchInput<'_>) -> bool {
116        let dns_query = input.dns_query.map(|v| v.to_ascii_lowercase());
117        if let Some(ref query) = dns_query
118            && self
119                .dns_suffixes
120                .iter()
121                .any(|suffix| query.ends_with(suffix))
122        {
123            return true;
124        }
125
126        let tls_sni = input.tls_sni.map(|v| v.to_ascii_lowercase());
127        if let Some(ref sni) = tls_sni
128            && self
129                .tls_sni_suffixes
130                .iter()
131                .any(|suffix| sni.ends_with(suffix))
132        {
133            return true;
134        }
135
136        if let Some(host) = input.http_host
137            && self
138                .http_hosts
139                .iter()
140                .any(|expected| host.eq_ignore_ascii_case(expected))
141        {
142            return true;
143        }
144
145        false
146    }
147}
148
149impl<'a> From<ApplicationDefinition<'a>> for ApplicationObj {
150    fn from(def: ApplicationDefinition<'a>) -> Self {
151        Self {
152            name: def.name.to_string(),
153            category: def.category.to_string(),
154            transports: def.transports.to_vec(),
155            dns_suffixes: def
156                .indicators
157                .dns_suffixes
158                .iter()
159                .map(|s| s.to_string())
160                .collect(),
161            tls_sni_suffixes: def
162                .indicators
163                .tls_sni_suffixes
164                .iter()
165                .map(|s| s.to_string())
166                .collect(),
167            http_hosts: def
168                .indicators
169                .http_hosts
170                .iter()
171                .map(|s| s.to_string())
172                .collect(),
173        }
174    }
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180
181    #[test]
182    fn matches_dns_and_http() {
183        let app = ApplicationDefinition {
184            name: "demo",
185            category: "qa",
186            transports: &[TransportService::tcp(443)],
187            indicators: ApplicationIndicators {
188                dns_suffixes: &[".demo.local"],
189                tls_sni_suffixes: &[".demo.local"],
190                http_hosts: &["demo.local"],
191            },
192        };
193
194        let dns = ApplicationMatchInput {
195            dns_query: Some("api.demo.local"),
196            ..Default::default()
197        };
198        assert!(app.matches(&dns));
199
200        let http = ApplicationMatchInput {
201            http_host: Some("Demo.Local"),
202            ..Default::default()
203        };
204        assert!(app.matches(&http));
205    }
206
207    #[test]
208    fn owned_application_matches_metadata() {
209        let def = ApplicationDefinition {
210            name: "owned",
211            category: "sample",
212            transports: &[TransportService::tcp(8443)],
213            indicators: ApplicationIndicators {
214                dns_suffixes: &[".owned.local"],
215                tls_sni_suffixes: &[],
216                http_hosts: &["owned.local"],
217            },
218        };
219        let obj: ApplicationObj = def.into();
220        let input = ApplicationMatchInput {
221            http_host: Some("owned.local"),
222            ..Default::default()
223        };
224        assert!(obj.matches(&input));
225    }
226}