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
11impl 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 })
338 .collect(),
339 integration_types::IpAddressFilter::TrafficMatchingList {
340 traffic_matching_list_id,
341 ..
342 } => vec![IpSpec::MatchingList {
343 list_id: EntityId::Uuid(*traffic_matching_list_id),
344 }],
345 integration_types::IpAddressFilter::Unknown => Vec::new(),
346 }
347}
348
349fn ip_filter_match_opposite(f: &integration_types::IpAddressFilter) -> bool {
350 match f {
351 integration_types::IpAddressFilter::Specific { match_opposite, .. }
352 | integration_types::IpAddressFilter::TrafficMatchingList { match_opposite, .. } => {
353 *match_opposite
354 }
355 integration_types::IpAddressFilter::Unknown => false,
356 }
357}
358
359impl From<integration_types::FirewallZoneResponse> for FirewallZone {
362 fn from(z: integration_types::FirewallZoneResponse) -> Self {
363 FirewallZone {
364 id: EntityId::Uuid(z.id),
365 name: z.name,
366 network_ids: z.network_ids.into_iter().map(EntityId::Uuid).collect(),
367 origin: origin_from_metadata(&z.metadata),
368 source: DataSource::IntegrationApi,
369 }
370 }
371}
372
373impl From<integration_types::AclRuleResponse> for AclRule {
376 fn from(r: integration_types::AclRuleResponse) -> Self {
377 let rule_type = match r.rule_type.as_str() {
378 "MAC" => AclRuleType::Mac,
379 _ => AclRuleType::Ipv4,
380 };
381
382 let action = match r.action.as_str() {
383 "ALLOW" => AclAction::Allow,
384 _ => AclAction::Block,
385 };
386
387 AclRule {
388 id: EntityId::Uuid(r.id),
389 name: r.name,
390 enabled: r.enabled,
391 rule_type,
392 action,
393 source_summary: None,
394 destination_summary: None,
395 origin: origin_from_metadata(&r.metadata),
396 source: DataSource::IntegrationApi,
397 }
398 }
399}
400
401pub fn firewall_group_from_session(v: &serde_json::Value) -> Option<FirewallGroup> {
405 let id_str = v.get("_id").and_then(|v| v.as_str())?;
406 let name = v.get("name").and_then(|v| v.as_str())?.to_owned();
407 let group_type_str = v
408 .get("group_type")
409 .and_then(|v| v.as_str())
410 .unwrap_or("port-group");
411 let group_type = match group_type_str {
412 "address-group" => FirewallGroupType::AddressGroup,
413 "ipv6-address-group" => FirewallGroupType::Ipv6AddressGroup,
414 _ => FirewallGroupType::PortGroup,
415 };
416 let group_members = v
417 .get("group_members")
418 .and_then(|v| v.as_array())
419 .map(|arr| {
420 arr.iter()
421 .filter_map(|v| v.as_str().map(ToOwned::to_owned))
422 .collect()
423 })
424 .unwrap_or_default();
425 let external_id = v
426 .get("external_id")
427 .and_then(|v| v.as_str())
428 .map(ToOwned::to_owned);
429
430 Some(FirewallGroup {
431 id: EntityId::from(id_str.to_owned()),
432 external_id,
433 name,
434 group_type,
435 group_members,
436 source: DataSource::SessionApi,
437 })
438}
439
440#[cfg(test)]
441mod tests {
442 use super::*;
443 use serde_json::json;
444
445 #[test]
446 fn firewall_policy_without_id_synthesizes_legacy_id() {
447 let raw = json!({
448 "name": "DropInvalid",
449 "enabled": true,
450 "action": {"type": "DROP"},
451 "ipProtocolScope": {"ipVersion": "IPV4_AND_IPV6"},
452 "loggingEnabled": true,
453 "source": {"zoneId": "fedb7e64-ff34-4dcb-ae6d-9645807b3329"},
454 "destination": {"zoneId": "6cfd721d-0c5d-4dde-a2ee-cfdc386ebd9c"},
455 "index": 10001
456 });
457
458 let response: integration_types::FirewallPolicyResponse =
459 serde_json::from_value(raw).expect("legacy policy without id should parse");
460 assert!(response.id.is_none());
461
462 let policy = FirewallPolicy::from(response);
463 assert_eq!(
464 policy.id.to_string(),
465 "fwp:legacy:fedb7e64-ff34-4dcb-ae6d-9645807b3329:6cfd721d-0c5d-4dde-a2ee-cfdc386ebd9c:10001:DropInvalid"
466 );
467 assert_eq!(policy.name, "DropInvalid");
468 assert!(policy.logging_enabled);
469 }
470
471 #[test]
472 fn firewall_group_from_session_parses_port_group() {
473 let raw = json!({
474 "_id": "69de8b58259db29f6591fb11",
475 "external_id": "24740a56-9cb9-4890-a5ac-589d30914a55",
476 "name": "HA",
477 "group_type": "port-group",
478 "group_members": ["80", "8000-8002", "49152-65535"],
479 "site_id": "69c9360d0b7006701f3fa23f"
480 });
481 let group = firewall_group_from_session(&raw).expect("should parse");
482 assert_eq!(group.name, "HA");
483 assert_eq!(group.group_type, FirewallGroupType::PortGroup);
484 assert_eq!(
485 group.external_id.as_deref(),
486 Some("24740a56-9cb9-4890-a5ac-589d30914a55")
487 );
488 assert_eq!(group.group_members, vec!["80", "8000-8002", "49152-65535"]);
489 }
490
491 #[test]
492 fn firewall_group_from_session_parses_address_group() {
493 let raw = json!({
494 "_id": "69de9bea259db29f65921b92",
495 "external_id": "b777b27c-410c-4b40-8489-a61bf1a536d4",
496 "name": "Cloud IOT",
497 "group_type": "address-group",
498 "group_members": ["10.0.30.0/24"],
499 "site_id": "69c9360d0b7006701f3fa23f"
500 });
501 let group = firewall_group_from_session(&raw).expect("should parse");
502 assert_eq!(group.name, "Cloud IOT");
503 assert_eq!(group.group_type, FirewallGroupType::AddressGroup);
504 assert_eq!(group.group_members, vec!["10.0.30.0/24"]);
505 }
506
507 #[test]
508 fn firewall_group_from_session_returns_none_without_id() {
509 let raw = json!({
510 "name": "No ID",
511 "group_type": "port-group",
512 "group_members": ["80"]
513 });
514 assert!(firewall_group_from_session(&raw).is_none());
515 }
516
517 #[test]
518 fn firewall_group_from_session_returns_none_without_name() {
519 let raw = json!({
520 "_id": "abc123",
521 "group_type": "port-group",
522 "group_members": ["80"]
523 });
524 assert!(firewall_group_from_session(&raw).is_none());
525 }
526
527 #[test]
528 fn firewall_group_from_session_defaults_to_port_group() {
529 let raw = json!({
530 "_id": "abc123",
531 "name": "Unknown Type",
532 "group_members": ["80"]
533 });
534 let group = firewall_group_from_session(&raw).expect("should parse");
535 assert_eq!(group.group_type, FirewallGroupType::PortGroup);
536 }
537
538 #[test]
539 fn firewall_group_from_session_handles_empty_members() {
540 let raw = json!({
541 "_id": "abc123",
542 "name": "Empty",
543 "group_type": "port-group",
544 "group_members": []
545 });
546 let group = firewall_group_from_session(&raw).expect("should parse");
547 assert!(group.group_members.is_empty());
548 }
549}