pan_os_rs/
lib.rs

1use regex::Regex;
2use reqwest::header::HeaderMap;
3use serde::{de::DeserializeOwned, Serialize};
4use serde_json::json;
5use std::fmt::Write;
6use std::{collections::HashMap, error::Error};
7use tracing::{debug, trace};
8use zeroize::Zeroizing;
9
10pub mod api;
11pub mod objects;
12pub mod panorama;
13pub mod policies;
14
15// Builds out the query parameters for a given URI
16fn form_params(args: HashMap<&str, &str>) -> String {
17    let mut params = String::new();
18    for (k, v) in args {
19        trace!("Writing key {k} with value {v} to query parameters");
20        if !params.starts_with('?') {
21            _ = write!(params, "?{k}={v}")
22        } else {
23            _ = write!(params, "&{k}={v}");
24        }
25    }
26
27    params
28}
29
30// Forms common query parameters
31fn common_params(
32    location: Option<api::Location>,
33    device_group: Option<&str>,
34    name: &str,
35) -> String {
36    let mut args = HashMap::new();
37    if let Some(location) = location {
38        args.insert("location", location.to_str());
39    }
40    if let Some(device_group) = device_group {
41        args.insert("device-group", device_group);
42    }
43    args.insert("name", name);
44
45    form_params(args)
46}
47
48pub struct Session {
49    pub client: reqwest::Client,
50    pub base_url: String,
51}
52
53impl Session {
54    // Using the provided username and password, logs into Panorama or a firewall directly
55    pub async fn new(
56        ip: &str,
57        user: &str,
58        password: String,
59        allow_insecure: bool,
60    ) -> Result<Session, Box<dyn Error>> {
61        let password = Zeroizing::new(password);
62
63        // Prepare regex for finding the API key within the XML response and retrieve the key
64        trace!("Building temp client to retrieve API key");
65        let temp_client = reqwest::Client::builder()
66            .danger_accept_invalid_certs(allow_insecure)
67            .build()?;
68
69        debug!("Sending request to {ip} as {user} for API key");
70        let key_re = Regex::new(r"<key>(.+)</key>").unwrap();
71        let key_resp = temp_client
72            .get(format!(
73                "https://{ip}/api?type=keygen&user={user}&password={}",
74                urlencoding::encode(password.as_str())
75            ))
76            .send()
77            .await?;
78
79        let mut content = key_resp.text().await?;
80        let mut captures = key_re.captures(&content).unwrap();
81        let api_key = if captures.len() > 1 {
82            &captures[1]
83        } else {
84            return Err(Box::new(api::Error {
85                code: 999,
86                message: String::from("Unable to find key, validate provided credentials"),
87            }));
88        };
89
90        trace!("Building client with API key as a default header");
91        // Configure default headers to send the key with every API call
92        let mut headers = HeaderMap::new();
93        headers.insert("X-PAN-KEY", api_key.parse()?);
94        let client = reqwest::Client::builder()
95            .default_headers(headers)
96            .danger_accept_invalid_certs(allow_insecure)
97            .build()?;
98
99        debug!("Sending request to {ip} as {user} for PanOS version");
100        // Prepare regex for finding the PanOS version within the XML response and retrieve the version
101        let vers_re = Regex::new(r"<sw-version>(\d+\.\d)(.+)?</sw-version>").unwrap();
102        let vers_resp = client
103            .get(format!("https://{ip}/api?type=version&key={api_key}"))
104            .send()
105            .await?;
106        content = vers_resp.text().await?;
107        captures = vers_re.captures(&content).unwrap();
108        let panos_version = if captures.len() > 1 {
109            &captures[1]
110        } else {
111            return Err(Box::new(api::Error {
112                code: 999,
113                message: String::from("Unable to find version, validate provided API key"),
114            }));
115        };
116
117        Ok(Self {
118            client,
119            base_url: format!("https://{ip}/restapi/v{panos_version}/"),
120        })
121    }
122
123    // Retrieves all instances of the provided object type
124    pub async fn get_all<T: DeserializeOwned + api::Endpoint>(
125        &self,
126        location: Option<api::Location>,
127        device_group: Option<&str>,
128    ) -> Result<Option<Vec<T>>, Box<dyn Error>> {
129        let params = {
130            let mut args = HashMap::new();
131            if let Some(location) = location {
132                args.insert("location", location.to_str());
133            }
134            if let Some(device_group) = device_group {
135                args.insert("device-group", device_group);
136            }
137
138            form_params(args)
139        };
140
141        let uri = T::get_uri();
142        debug!("Sending request to endpoint {uri}");
143        let resp = self
144            .client
145            .get(format!("{}{uri}{params}", self.base_url))
146            .send()
147            .await?;
148        if !resp.status().is_success() {
149            return Err(Box::new(resp.json::<api::Error>().await?));
150        }
151
152        let api_resp: api::ApiResponse<T> = resp.json().await?;
153        if api_resp.result.entry.is_some() {
154            Ok(Some(api_resp.result.entry.unwrap()))
155        } else {
156            Ok(None)
157        }
158    }
159
160    // Retrieves an instance of the provided object type
161    pub async fn get<T: DeserializeOwned + api::Endpoint>(
162        &self,
163        location: Option<api::Location>,
164        device_group: Option<&str>,
165        name: &str,
166    ) -> Result<Option<T>, Box<dyn Error>> {
167        let params = common_params(location, device_group, name);
168
169        let uri = T::get_uri();
170        debug!("Sending request to endpoint {uri}");
171        let resp = self
172            .client
173            .get(format!("{}{uri}{params}", self.base_url))
174            .send()
175            .await?;
176        if !resp.status().is_success() {
177            return Err(Box::new(resp.json::<api::Error>().await?));
178        }
179
180        let api_resp: api::ApiResponse<T> = resp.json().await?;
181        let entries = api_resp.result.entry.unwrap_or_default();
182        if entries.len() == 1 {
183            Ok(Some(entries.into_iter().next().unwrap()))
184        } else {
185            Ok(None)
186        }
187    }
188
189    // Creates a new instance of the provided object type
190    pub async fn create<T: Serialize + api::Endpoint>(
191        &self,
192        location: Option<api::Location>,
193        device_group: Option<&str>,
194        name: &str,
195        obj: T,
196    ) -> Result<(), Box<dyn Error>> {
197        let params = common_params(location, device_group, name);
198
199        let uri = T::get_uri();
200        debug!("Sending request to endpoint {uri}");
201        let resp = self
202            .client
203            .post(format!("{}{uri}{params}", self.base_url))
204            .json(&json!({ "entry": obj }))
205            .send()
206            .await?;
207        if !resp.status().is_success() {
208            return Err(Box::new(resp.json::<api::Error>().await?));
209        }
210
211        Ok(())
212    }
213
214    // Edits an existing instance of the provided object type
215    pub async fn edit<T: Serialize + api::Endpoint>(
216        &self,
217        location: Option<api::Location>,
218        device_group: Option<&str>,
219        name: &str,
220        obj: T,
221    ) -> Result<(), Box<dyn Error>> {
222        let params = common_params(location, device_group, name);
223
224        let uri = T::get_uri();
225        debug!("Sending request to endpoint {uri}");
226        let resp = self
227            .client
228            .put(format!("{}{uri}{params}", self.base_url))
229            .json(&json!({ "entry": obj }))
230            .send()
231            .await?;
232        if !resp.status().is_success() {
233            return Err(Box::new(resp.json::<api::Error>().await?));
234        }
235
236        Ok(())
237    }
238
239    // Deletes an existing instance of the provided object type
240    pub async fn delete<T: api::Endpoint>(
241        &self,
242        location: Option<api::Location>,
243        device_group: Option<&str>,
244        name: &str,
245    ) -> Result<(), Box<dyn Error>> {
246        let params = common_params(location, device_group, name);
247
248        let uri = T::get_uri();
249        debug!("Sending request to endpoint {uri}");
250        let resp = self
251            .client
252            .delete(format!("{}{uri}{params}", self.base_url))
253            .send()
254            .await?;
255        if !resp.status().is_success() {
256            return Err(Box::new(resp.json::<api::Error>().await?));
257        }
258
259        Ok(())
260    }
261
262    // Renames an existing instance of the provided object type
263    pub async fn rename<T: api::Endpoint>(
264        &self,
265        location: api::Location,
266        device_group: Option<&str>,
267        name: &str,
268        new_name: &str,
269    ) -> Result<(), Box<dyn Error>> {
270        let params = {
271            let mut args = HashMap::new();
272            args.insert("location", location.to_str());
273            if let Some(device_group) = device_group {
274                args.insert("device-group", device_group);
275            }
276            args.insert("name", name);
277            args.insert("new-name", new_name);
278
279            form_params(args)
280        };
281
282        let uri = T::get_uri();
283        debug!("Sending request to endpoint {uri}");
284        let resp = self
285            .client
286            .post(format!("{}{uri}:rename{params}", self.base_url))
287            .send()
288            .await?;
289        if !resp.status().is_success() {
290            return Err(Box::new(resp.json::<api::Error>().await?));
291        }
292
293        Ok(())
294    }
295
296    // Moves an existing instance of the provided object type
297    pub async fn relocate<T: api::Endpoint>(
298        &self,
299        location: api::Location,
300        device_group: Option<&str>,
301        where_to: api::Where,
302        dst: Option<&str>,
303    ) -> Result<(), Box<dyn Error>> {
304        let params = {
305            let mut args = HashMap::new();
306            args.insert("location", location.to_str());
307            if let Some(device_group) = device_group {
308                args.insert("device-group", device_group);
309            }
310            args.insert("where", where_to.to_str());
311
312            match where_to {
313                api::Where::Before | api::Where::After => {
314                    if let Some(dst) = dst {
315                        args.insert("dst", dst);
316                    } else {
317                        let err = api::Error {
318                            code: 999,
319                            message: String::from("dst is required when using before or after"),
320                        };
321
322                        return Err(Box::new(err));
323                    }
324                }
325                _ => (),
326            }
327
328            form_params(args)
329        };
330
331        let uri = T::get_uri();
332        debug!("Sending request to endpoint {uri}");
333        let resp = self
334            .client
335            .post(format!("{}{uri}:move{params}", self.base_url))
336            .send()
337            .await?;
338        if !resp.status().is_success() {
339            return Err(Box::new(resp.json::<api::Error>().await?));
340        }
341
342        Ok(())
343    }
344}