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