Skip to main content

unifly_api/convert/
firewall.rs

1use crate::integration_types;
2use crate::model::common::DataSource;
3use crate::model::entity_id::EntityId;
4use crate::model::firewall::{
5    AclAction, AclRule, AclRuleType, FirewallAction, FirewallGroup, FirewallGroupType,
6    FirewallPolicy, FirewallZone, IpSpec, PolicyEndpoint, PortSpec, TrafficFilter,
7};
8
9use super::helpers::origin_from_metadata;
10
11// ── Firewall Policy ──────────────────────────────────────────────
12
13impl From<integration_types::FirewallPolicyResponse> for FirewallPolicy {
14    fn from(p: integration_types::FirewallPolicyResponse) -> Self {
15        let action = p.action.get("type").and_then(|v| v.as_str()).map_or(
16            FirewallAction::Block,
17            |a| match a {
18                "ALLOW" => FirewallAction::Allow,
19                "REJECT" => FirewallAction::Reject,
20                _ => FirewallAction::Block,
21            },
22        );
23
24        #[allow(clippy::as_conversions, clippy::cast_possible_truncation)]
25        let index = p
26            .extra
27            .get("index")
28            .and_then(serde_json::Value::as_i64)
29            .map(|i| i as i32);
30
31        let source_endpoint =
32            convert_policy_endpoint(p.source.as_ref(), p.extra.get("sourceFirewallZoneId"));
33        let destination_endpoint = convert_dest_policy_endpoint(
34            p.destination.as_ref(),
35            p.extra.get("destinationFirewallZoneId"),
36        );
37
38        let source_summary = source_endpoint.filter.as_ref().map(TrafficFilter::summary);
39        let destination_summary = destination_endpoint
40            .filter
41            .as_ref()
42            .map(TrafficFilter::summary);
43
44        let ip_version = p
45            .ip_protocol_scope
46            .as_ref()
47            .and_then(|v| v.get("ipVersion"))
48            .and_then(|v| v.as_str())
49            .map_or(crate::model::firewall::IpVersion::Both, |s| match s {
50                "IPV4_ONLY" | "IPV4" => crate::model::firewall::IpVersion::Ipv4,
51                "IPV6_ONLY" | "IPV6" => crate::model::firewall::IpVersion::Ipv6,
52                _ => crate::model::firewall::IpVersion::Both,
53            });
54
55        let ipsec_mode = p
56            .extra
57            .get("ipsecFilter")
58            .and_then(|v| v.as_str())
59            .map(String::from);
60
61        let connection_states = p
62            .extra
63            .get("connectionStateFilter")
64            .and_then(|v| v.as_array())
65            .map(|arr| {
66                arr.iter()
67                    .filter_map(|v| v.as_str().map(String::from))
68                    .collect()
69            })
70            .unwrap_or_default();
71
72        let id = p.id.map_or_else(
73            || synthesize_legacy_policy_id(&source_endpoint, &destination_endpoint, index, &p.name),
74            EntityId::Uuid,
75        );
76
77        FirewallPolicy {
78            id,
79            name: p.name,
80            description: p.description,
81            enabled: p.enabled,
82            index,
83            action,
84            ip_version,
85            source: source_endpoint,
86            destination: destination_endpoint,
87            source_summary,
88            destination_summary,
89            protocol_summary: None,
90            schedule: None,
91            ipsec_mode,
92            connection_states,
93            logging_enabled: p.logging_enabled,
94            origin: p.metadata.as_ref().and_then(origin_from_metadata),
95            data_source: DataSource::IntegrationApi,
96        }
97    }
98}
99
100fn synthesize_legacy_policy_id(
101    source: &PolicyEndpoint,
102    destination: &PolicyEndpoint,
103    index: Option<i32>,
104    name: &str,
105) -> EntityId {
106    let src = source
107        .zone_id
108        .as_ref()
109        .map_or_else(|| "none".to_owned(), ToString::to_string);
110    let dst = destination
111        .zone_id
112        .as_ref()
113        .map_or_else(|| "none".to_owned(), ToString::to_string);
114    let idx = index.map_or_else(|| "noindex".to_owned(), |i| i.to_string());
115    EntityId::Legacy(format!("fwp:legacy:{src}:{dst}:{idx}:{name}"))
116}
117
118fn convert_policy_endpoint(
119    endpoint: Option<&integration_types::FirewallPolicySource>,
120    flat_zone_id: Option<&serde_json::Value>,
121) -> PolicyEndpoint {
122    if let Some(ep) = endpoint {
123        PolicyEndpoint {
124            zone_id: ep.zone_id.map(EntityId::Uuid),
125            filter: ep
126                .traffic_filter
127                .as_ref()
128                .map(convert_source_traffic_filter),
129        }
130    } else {
131        let zone_id = flat_zone_id
132            .and_then(|v| v.as_str())
133            .and_then(|s| uuid::Uuid::parse_str(s).ok())
134            .map(EntityId::Uuid);
135        PolicyEndpoint {
136            zone_id,
137            filter: None,
138        }
139    }
140}
141
142fn convert_dest_policy_endpoint(
143    endpoint: Option<&integration_types::FirewallPolicyDestination>,
144    flat_zone_id: Option<&serde_json::Value>,
145) -> PolicyEndpoint {
146    if let Some(ep) = endpoint {
147        PolicyEndpoint {
148            zone_id: ep.zone_id.map(EntityId::Uuid),
149            filter: ep.traffic_filter.as_ref().map(convert_dest_traffic_filter),
150        }
151    } else {
152        let zone_id = flat_zone_id
153            .and_then(|v| v.as_str())
154            .and_then(|s| uuid::Uuid::parse_str(s).ok())
155            .map(EntityId::Uuid);
156        PolicyEndpoint {
157            zone_id,
158            filter: None,
159        }
160    }
161}
162
163fn convert_source_traffic_filter(f: &integration_types::SourceTrafficFilter) -> TrafficFilter {
164    use integration_types::SourceTrafficFilter as S;
165    match f {
166        S::Network {
167            network_filter,
168            mac_address_filter,
169            port_filter,
170        } => TrafficFilter::Network {
171            network_ids: network_filter
172                .network_ids
173                .iter()
174                .copied()
175                .map(EntityId::Uuid)
176                .collect(),
177            match_opposite: network_filter.match_opposite,
178            mac_addresses: mac_address_filter
179                .as_ref()
180                .map(|m| m.mac_addresses.clone())
181                .unwrap_or_default(),
182            ports: port_filter.as_ref().map(convert_port_filter),
183        },
184        S::IpAddress {
185            ip_address_filter,
186            mac_address_filter,
187            port_filter,
188        } => TrafficFilter::IpAddress {
189            addresses: convert_ip_address_filter(ip_address_filter),
190            match_opposite: ip_filter_match_opposite(ip_address_filter),
191            mac_addresses: mac_address_filter
192                .as_ref()
193                .map(|m| m.mac_addresses.clone())
194                .unwrap_or_default(),
195            ports: port_filter.as_ref().map(convert_port_filter),
196        },
197        S::MacAddress {
198            mac_address_filter,
199            port_filter,
200        } => TrafficFilter::MacAddress {
201            mac_addresses: mac_address_filter.mac_addresses.clone(),
202            ports: port_filter.as_ref().map(convert_port_filter),
203        },
204        S::Port { port_filter } => TrafficFilter::Port {
205            ports: convert_port_filter(port_filter),
206        },
207        S::Region {
208            region_filter,
209            port_filter,
210        } => TrafficFilter::Region {
211            regions: region_filter.regions.clone(),
212            ports: port_filter.as_ref().map(convert_port_filter),
213        },
214        S::Unknown => TrafficFilter::Other {
215            raw_type: "UNKNOWN".into(),
216        },
217    }
218}
219
220fn convert_dest_traffic_filter(f: &integration_types::DestTrafficFilter) -> TrafficFilter {
221    use integration_types::DestTrafficFilter as D;
222    match f {
223        D::Network {
224            network_filter,
225            port_filter,
226        } => TrafficFilter::Network {
227            network_ids: network_filter
228                .network_ids
229                .iter()
230                .copied()
231                .map(EntityId::Uuid)
232                .collect(),
233            match_opposite: network_filter.match_opposite,
234            mac_addresses: Vec::new(),
235            ports: port_filter.as_ref().map(convert_port_filter),
236        },
237        D::IpAddress {
238            ip_address_filter,
239            port_filter,
240        } => TrafficFilter::IpAddress {
241            addresses: convert_ip_address_filter(ip_address_filter),
242            match_opposite: ip_filter_match_opposite(ip_address_filter),
243            mac_addresses: Vec::new(),
244            ports: port_filter.as_ref().map(convert_port_filter),
245        },
246        D::Port { port_filter } => TrafficFilter::Port {
247            ports: convert_port_filter(port_filter),
248        },
249        D::Region {
250            region_filter,
251            port_filter,
252        } => TrafficFilter::Region {
253            regions: region_filter.regions.clone(),
254            ports: port_filter.as_ref().map(convert_port_filter),
255        },
256        D::Application {
257            application_filter,
258            port_filter,
259        } => TrafficFilter::Application {
260            application_ids: application_filter.application_ids.clone(),
261            ports: port_filter.as_ref().map(convert_port_filter),
262        },
263        D::ApplicationCategory {
264            application_category_filter,
265            port_filter,
266        } => TrafficFilter::ApplicationCategory {
267            category_ids: application_category_filter.application_category_ids.clone(),
268            ports: port_filter.as_ref().map(convert_port_filter),
269        },
270        D::Domain {
271            domain_filter,
272            port_filter,
273        } => {
274            let domains = match domain_filter {
275                integration_types::DomainFilter::Specific { domains } => domains.clone(),
276                integration_types::DomainFilter::Unknown => Vec::new(),
277            };
278            TrafficFilter::Domain {
279                domains,
280                ports: port_filter.as_ref().map(convert_port_filter),
281            }
282        }
283        D::Unknown => TrafficFilter::Other {
284            raw_type: "UNKNOWN".into(),
285        },
286    }
287}
288
289fn convert_port_filter(pf: &integration_types::PortFilter) -> PortSpec {
290    match pf {
291        integration_types::PortFilter::Ports {
292            items,
293            match_opposite,
294        } => PortSpec::Values {
295            items: items
296                .iter()
297                .map(|item| match item {
298                    integration_types::PortItem::Number { value } => value.clone(),
299                    integration_types::PortItem::Range {
300                        start_port,
301                        end_port,
302                    } => format!("{start_port}-{end_port}"),
303                    integration_types::PortItem::Unknown => "?".into(),
304                })
305                .collect(),
306            match_opposite: *match_opposite,
307        },
308        integration_types::PortFilter::TrafficMatchingList {
309            traffic_matching_list_id,
310            match_opposite,
311        } => PortSpec::MatchingList {
312            list_id: EntityId::Uuid(*traffic_matching_list_id),
313            match_opposite: *match_opposite,
314        },
315        integration_types::PortFilter::Unknown => PortSpec::Values {
316            items: Vec::new(),
317            match_opposite: false,
318        },
319    }
320}
321
322fn convert_ip_address_filter(f: &integration_types::IpAddressFilter) -> Vec<IpSpec> {
323    match f {
324        integration_types::IpAddressFilter::Specific { items, .. } => items
325            .iter()
326            .map(|item| match item {
327                integration_types::IpAddressItem::Address { value } => IpSpec::Address {
328                    value: value.clone(),
329                },
330                integration_types::IpAddressItem::Range { start, stop } => IpSpec::Range {
331                    start: start.clone(),
332                    stop: stop.clone(),
333                },
334                integration_types::IpAddressItem::Subnet { value } => IpSpec::Subnet {
335                    value: value.clone(),
336                },
337                integration_types::IpAddressItem::Unknown => IpSpec::Address { value: "?".into() },
338            })
339            .collect(),
340        integration_types::IpAddressFilter::TrafficMatchingList {
341            traffic_matching_list_id,
342            ..
343        } => vec![IpSpec::MatchingList {
344            list_id: EntityId::Uuid(*traffic_matching_list_id),
345        }],
346        integration_types::IpAddressFilter::Unknown => Vec::new(),
347    }
348}
349
350fn ip_filter_match_opposite(f: &integration_types::IpAddressFilter) -> bool {
351    match f {
352        integration_types::IpAddressFilter::Specific { match_opposite, .. }
353        | integration_types::IpAddressFilter::TrafficMatchingList { match_opposite, .. } => {
354            *match_opposite
355        }
356        integration_types::IpAddressFilter::Unknown => false,
357    }
358}
359
360// ── Firewall Zone ────────────────────────────────────────────────
361
362impl From<integration_types::FirewallZoneResponse> for FirewallZone {
363    fn from(z: integration_types::FirewallZoneResponse) -> Self {
364        FirewallZone {
365            id: EntityId::Uuid(z.id),
366            name: z.name,
367            network_ids: z.network_ids.into_iter().map(EntityId::Uuid).collect(),
368            origin: origin_from_metadata(&z.metadata),
369            source: DataSource::IntegrationApi,
370        }
371    }
372}
373
374// ── ACL Rule ─────────────────────────────────────────────────────
375
376impl From<integration_types::AclRuleResponse> for AclRule {
377    fn from(r: integration_types::AclRuleResponse) -> Self {
378        let rule_type = match r.rule_type.as_str() {
379            "MAC" => AclRuleType::Mac,
380            _ => AclRuleType::Ipv4,
381        };
382
383        let action = match r.action.as_str() {
384            "ALLOW" => AclAction::Allow,
385            _ => AclAction::Block,
386        };
387
388        AclRule {
389            id: EntityId::Uuid(r.id),
390            name: r.name,
391            enabled: r.enabled,
392            rule_type,
393            action,
394            source_summary: None,
395            destination_summary: None,
396            origin: origin_from_metadata(&r.metadata),
397            source: DataSource::IntegrationApi,
398        }
399    }
400}
401
402// ── Firewall Group ──────────────────────────────────────────────
403
404/// Parse a firewall group from a `rest/firewallgroup` Session API response.
405pub fn firewall_group_from_session(v: &serde_json::Value) -> Option<FirewallGroup> {
406    let id_str = v.get("_id").and_then(|v| v.as_str())?;
407    let name = v.get("name").and_then(|v| v.as_str())?.to_owned();
408    let group_type_str = v
409        .get("group_type")
410        .and_then(|v| v.as_str())
411        .unwrap_or("port-group");
412    let group_type = match group_type_str {
413        "address-group" => FirewallGroupType::AddressGroup,
414        "ipv6-address-group" => FirewallGroupType::Ipv6AddressGroup,
415        _ => FirewallGroupType::PortGroup,
416    };
417    let group_members = v
418        .get("group_members")
419        .and_then(|v| v.as_array())
420        .map(|arr| {
421            arr.iter()
422                .filter_map(|v| v.as_str().map(ToOwned::to_owned))
423                .collect()
424        })
425        .unwrap_or_default();
426    let external_id = v
427        .get("external_id")
428        .and_then(|v| v.as_str())
429        .map(ToOwned::to_owned);
430
431    Some(FirewallGroup {
432        id: EntityId::from(id_str.to_owned()),
433        external_id,
434        name,
435        group_type,
436        group_members,
437        source: DataSource::SessionApi,
438    })
439}
440
441#[cfg(test)]
442mod tests {
443    use super::*;
444    use serde_json::json;
445
446    #[test]
447    fn firewall_policy_without_id_synthesizes_legacy_id() {
448        let raw = json!({
449            "name": "DropInvalid",
450            "enabled": true,
451            "action": {"type": "DROP"},
452            "ipProtocolScope": {"ipVersion": "IPV4_AND_IPV6"},
453            "loggingEnabled": true,
454            "source": {"zoneId": "fedb7e64-ff34-4dcb-ae6d-9645807b3329"},
455            "destination": {"zoneId": "6cfd721d-0c5d-4dde-a2ee-cfdc386ebd9c"},
456            "index": 10001
457        });
458
459        let response: integration_types::FirewallPolicyResponse =
460            serde_json::from_value(raw).expect("legacy policy without id should parse");
461        assert!(response.id.is_none());
462
463        let policy = FirewallPolicy::from(response);
464        assert_eq!(
465            policy.id.to_string(),
466            "fwp:legacy:fedb7e64-ff34-4dcb-ae6d-9645807b3329:6cfd721d-0c5d-4dde-a2ee-cfdc386ebd9c:10001:DropInvalid"
467        );
468        assert_eq!(policy.name, "DropInvalid");
469        assert!(policy.logging_enabled);
470    }
471
472    #[test]
473    fn firewall_group_from_session_parses_port_group() {
474        let raw = json!({
475            "_id": "69de8b58259db29f6591fb11",
476            "external_id": "24740a56-9cb9-4890-a5ac-589d30914a55",
477            "name": "HA",
478            "group_type": "port-group",
479            "group_members": ["80", "8000-8002", "49152-65535"],
480            "site_id": "69c9360d0b7006701f3fa23f"
481        });
482        let group = firewall_group_from_session(&raw).expect("should parse");
483        assert_eq!(group.name, "HA");
484        assert_eq!(group.group_type, FirewallGroupType::PortGroup);
485        assert_eq!(
486            group.external_id.as_deref(),
487            Some("24740a56-9cb9-4890-a5ac-589d30914a55")
488        );
489        assert_eq!(group.group_members, vec!["80", "8000-8002", "49152-65535"]);
490    }
491
492    #[test]
493    fn firewall_group_from_session_parses_address_group() {
494        let raw = json!({
495            "_id": "69de9bea259db29f65921b92",
496            "external_id": "b777b27c-410c-4b40-8489-a61bf1a536d4",
497            "name": "Cloud IOT",
498            "group_type": "address-group",
499            "group_members": ["10.0.30.0/24"],
500            "site_id": "69c9360d0b7006701f3fa23f"
501        });
502        let group = firewall_group_from_session(&raw).expect("should parse");
503        assert_eq!(group.name, "Cloud IOT");
504        assert_eq!(group.group_type, FirewallGroupType::AddressGroup);
505        assert_eq!(group.group_members, vec!["10.0.30.0/24"]);
506    }
507
508    #[test]
509    fn firewall_group_from_session_returns_none_without_id() {
510        let raw = json!({
511            "name": "No ID",
512            "group_type": "port-group",
513            "group_members": ["80"]
514        });
515        assert!(firewall_group_from_session(&raw).is_none());
516    }
517
518    #[test]
519    fn firewall_group_from_session_returns_none_without_name() {
520        let raw = json!({
521            "_id": "abc123",
522            "group_type": "port-group",
523            "group_members": ["80"]
524        });
525        assert!(firewall_group_from_session(&raw).is_none());
526    }
527
528    #[test]
529    fn firewall_group_from_session_defaults_to_port_group() {
530        let raw = json!({
531            "_id": "abc123",
532            "name": "Unknown Type",
533            "group_members": ["80"]
534        });
535        let group = firewall_group_from_session(&raw).expect("should parse");
536        assert_eq!(group.group_type, FirewallGroupType::PortGroup);
537    }
538
539    #[test]
540    fn firewall_group_from_session_handles_empty_members() {
541        let raw = json!({
542            "_id": "abc123",
543            "name": "Empty",
544            "group_type": "port-group",
545            "group_members": []
546        });
547        let group = firewall_group_from_session(&raw).expect("should parse");
548        assert!(group.group_members.is_empty());
549    }
550}