1use crate::cassette::Cassette;
2use crate::filter::FilterChain;
3use crate::serializable::{SerializableRequest, SerializableResponse};
4use http_client::Error;
5use std::path::PathBuf;
6
7pub async fn filter_cassette_file<P: Into<PathBuf>>(
10 cassette_path: P,
11 filter_chain: FilterChain,
12) -> Result<(), Error> {
13 let path = cassette_path.into();
14
15 let mut cassette = Cassette::load_from_file(path.clone()).await?;
17
18 for interaction in &mut cassette.interactions {
20 filter_chain.filter_request(&mut interaction.request);
21 filter_chain.filter_response(&mut interaction.response);
22 }
23
24 cassette.save_to_file().await?;
26
27 log::debug!(
28 "Applied filters to {} interactions in {path:?}",
29 cassette.interactions.len()
30 );
31 Ok(())
32}
33
34pub async fn mutate_all_requests<P, F>(cassette_path: P, mut mutator: F) -> Result<(), Error>
37where
38 P: Into<PathBuf>,
39 F: FnMut(&mut SerializableRequest),
40{
41 let path = cassette_path.into();
42 let mut cassette = Cassette::load_from_file(path.clone()).await?;
43
44 for interaction in &mut cassette.interactions {
45 mutator(&mut interaction.request);
46 }
47
48 cassette.save_to_file().await?;
49 log::debug!(
50 "Applied custom mutations to {} requests in {path:?}",
51 cassette.interactions.len()
52 );
53 Ok(())
54}
55
56pub async fn mutate_all_responses<P, F>(cassette_path: P, mut mutator: F) -> Result<(), Error>
58where
59 P: Into<PathBuf>,
60 F: FnMut(&mut SerializableResponse),
61{
62 let path = cassette_path.into();
63 let mut cassette = Cassette::load_from_file(path.clone()).await?;
64
65 for interaction in &mut cassette.interactions {
66 mutator(&mut interaction.response);
67 }
68
69 cassette.save_to_file().await?;
70 log::debug!(
71 "Applied custom mutations to {} responses in {path:?}",
72 cassette.interactions.len()
73 );
74 Ok(())
75}
76
77pub async fn mutate_all_interactions<P, RF, ResF>(
79 cassette_path: P,
80 mut request_mutator: RF,
81 mut response_mutator: ResF,
82) -> Result<(), Error>
83where
84 P: Into<PathBuf>,
85 RF: FnMut(&mut SerializableRequest),
86 ResF: FnMut(&mut SerializableResponse),
87{
88 let path = cassette_path.into();
89 let mut cassette = Cassette::load_from_file(path.clone()).await?;
90
91 for interaction in &mut cassette.interactions {
92 request_mutator(&mut interaction.request);
93 response_mutator(&mut interaction.response);
94 }
95
96 cassette.save_to_file().await?;
97 log::debug!(
98 "Applied custom mutations to {} interactions in {path:?}",
99 cassette.interactions.len()
100 );
101 Ok(())
102}
103
104pub async fn strip_all_credentials_from_requests<P: Into<PathBuf>>(
106 cassette_path: P,
107) -> Result<(), Error> {
108 mutate_all_requests(cassette_path, |request| {
109 if let Some(body) = &mut request.body {
110 if body.contains('=') && (body.contains('&') || !body.contains(' ')) {
112 let filtered = crate::form_data::filter_form_data(body, "[REMOVED]");
113 *body = filtered;
114 }
115 }
116 })
117 .await
118}
119
120pub async fn strip_all_cookies<P: Into<PathBuf>>(cassette_path: P) -> Result<(), Error> {
122 mutate_all_interactions(
123 cassette_path,
124 |request| {
125 request.headers.remove("cookie");
126 request.headers.remove("Cookie");
127 },
128 |response| {
129 response.headers.remove("set-cookie");
130 response.headers.remove("Set-Cookie");
131 },
132 )
133 .await
134}
135
136pub async fn replace_form_field_in_all_requests<P: Into<PathBuf>>(
138 cassette_path: P,
139 field_name: &str,
140 replacement_value: &str,
141) -> Result<(), Error> {
142 let field = field_name.to_string();
143 let replacement = replacement_value.to_string();
144
145 mutate_all_requests(cassette_path, move |request| {
146 if let Some(body) = &mut request.body {
147 if body.contains('=') && (body.contains('&') || !body.contains(' ')) {
148 let mut params = crate::form_data::parse_form_data(body);
149 if params.contains_key(&field) {
150 params.insert(field.clone(), replacement.clone());
151 *body = crate::form_data::encode_form_data(¶ms);
152 }
153 }
154 }
155 })
156 .await
157}
158
159pub async fn remove_header_from_all_requests<P: Into<PathBuf>>(
161 cassette_path: P,
162 header_name: &str,
163) -> Result<(), Error> {
164 let header = header_name.to_string();
165
166 mutate_all_requests(cassette_path, move |request| {
167 request.headers.remove(&header);
168 request.headers.remove(&header.to_lowercase());
170 })
171 .await
172}
173
174pub async fn replace_header_in_all_requests<P: Into<PathBuf>>(
176 cassette_path: P,
177 header_name: &str,
178 replacement_value: &str,
179) -> Result<(), Error> {
180 let header = header_name.to_string();
181 let replacement = replacement_value.to_string();
182
183 mutate_all_requests(cassette_path, move |request| {
184 if request.headers.contains_key(&header) {
185 request
186 .headers
187 .insert(header.clone(), vec![replacement.clone()]);
188 }
189 let header_lower = header.to_lowercase();
191 if request.headers.contains_key(&header_lower) {
192 request
193 .headers
194 .insert(header_lower, vec![replacement.clone()]);
195 }
196 })
197 .await
198}
199
200pub async fn scrub_urls_in_all_requests<P: Into<PathBuf>, F>(
202 cassette_path: P,
203 mut url_mutator: F,
204) -> Result<(), Error>
205where
206 F: FnMut(&str) -> String,
207{
208 mutate_all_requests(cassette_path, move |request| {
209 request.url = url_mutator(&request.url);
210 })
211 .await
212}
213
214pub async fn replace_username_in_all_requests<P: Into<PathBuf>>(
216 cassette_path: P,
217 new_username: &str,
218) -> Result<(), Error> {
219 let replacement = new_username.to_string();
220
221 mutate_all_requests(cassette_path, move |request| {
222 if let Some(body) = &mut request.body {
224 if body.contains('=') && (body.contains('&') || !body.contains(' ')) {
225 let mut params = crate::form_data::parse_form_data(body);
226
227 let username_fields = ["username", "user", "username_or_email", "email", "login"];
229 for field in &username_fields {
230 if params.contains_key(*field) {
231 params.insert(field.to_string(), replacement.clone());
232 }
233 }
234
235 *body = crate::form_data::encode_form_data(¶ms);
236 }
237 }
238
239 if let Some(auth_headers) = request.headers.get_mut("authorization") {
241 for auth_header in auth_headers.iter_mut() {
242 if auth_header.starts_with("Basic ") {
243 *auth_header = "[FILTERED_BASIC_AUTH]".to_string();
246 }
247 }
248 }
249 })
250 .await
251}
252
253pub async fn sanitize_cassette_for_sharing<P: Into<PathBuf>>(
255 cassette_path: P,
256) -> Result<(), Error> {
257 let path = cassette_path.into();
258
259 log::debug!("๐งน Sanitizing cassette for sharing: {path:?}");
260
261 let analysis = analyze_cassette_file(&path).await?;
263 analysis.print_report();
264
265 log::debug!("\n๐ง Applying sanitization...");
266
267 mutate_all_interactions(
269 &path,
270 |request| {
271 request.headers.remove("authorization");
273 request.headers.remove("Authorization");
274
275 if let Some(body) = &mut request.body {
277 if body.contains('=') && (body.contains('&') || !body.contains(' ')) {
278 *body = crate::form_data::filter_form_data(body, "[SANITIZED]");
279 }
280 }
281
282 if let Ok(mut url) = url::Url::parse(&request.url) {
284 let sensitive_params = ["api_key", "access_token", "key"];
285 let query_pairs: Vec<(String, String)> = url
286 .query_pairs()
287 .filter(|(key, _)| !sensitive_params.contains(&key.as_ref()))
288 .map(|(k, v)| (k.to_string(), v.to_string()))
289 .collect();
290
291 url.query_pairs_mut().clear();
292 for (key, value) in query_pairs {
293 url.query_pairs_mut().append_pair(&key, &value);
294 }
295
296 request.url = url.to_string();
297 }
298 },
299 |response| {
300 if let Some(body) = &mut response.body {
304 *body = body.replace(r#""sessionid":"[^"]*""#, r#""sessionid":"[SANITIZED]""#);
306 }
307 },
308 )
309 .await?;
310
311 log::debug!("โ
Cassette sanitized successfully!");
312 log::debug!("๐ All credentials, session data, and sensitive headers have been removed");
313
314 Ok(())
315}
316
317pub async fn analyze_cassette_file<P: Into<PathBuf>>(
320 cassette_path: P,
321) -> Result<CassetteAnalysis, Error> {
322 let path = cassette_path.into();
323 let cassette = Cassette::load_from_file(path.clone()).await?;
324
325 let mut analysis = CassetteAnalysis {
326 file_path: path,
327 total_interactions: cassette.interactions.len(),
328 requests_with_form_data: Vec::new(),
329 requests_with_credentials: Vec::new(),
330 sensitive_headers: Vec::new(),
331 };
332
333 for (i, interaction) in cassette.interactions.iter().enumerate() {
334 if let Some(body) = &interaction.request.body {
336 if body.contains('=') && (body.contains('&') || !body.contains(' ')) {
337 let form_analysis = crate::form_data::analyze_form_data(body);
338 if !form_analysis.credential_fields.is_empty() {
339 analysis.requests_with_form_data.push(i);
340 analysis
341 .requests_with_credentials
342 .push((i, form_analysis.credential_fields));
343 }
344 }
345 }
346
347 for (header_name, header_values) in &interaction.request.headers {
349 let header_lower = header_name.to_lowercase();
350 if header_lower.contains("cookie")
351 || header_lower.contains("authorization")
352 || header_lower.contains("token")
353 {
354 analysis
355 .sensitive_headers
356 .push((i, header_name.clone(), header_values.clone()));
357 }
358 }
359
360 for (header_name, header_values) in &interaction.response.headers {
362 let header_lower = header_name.to_lowercase();
363 if header_lower.contains("set-cookie")
364 || header_lower.contains("authorization")
365 || header_lower.contains("token")
366 {
367 analysis.sensitive_headers.push((
368 i,
369 format!("response-{header_name}"),
370 header_values.clone(),
371 ));
372 }
373 }
374 }
375
376 Ok(analysis)
377}
378
379pub async fn set_test_password_in_cassette<P: Into<PathBuf>>(
382 cassette_path: P,
383 test_password: &str,
384) -> Result<(), Error> {
385 let path = cassette_path.into();
386 let password = test_password.to_string();
387
388 log::debug!("๐ Setting test password in cassette: {path:?}");
389
390 mutate_all_requests(&path, move |request| {
391 if let Some(body) = &mut request.body {
392 if body.contains('=') && (body.contains('&') || !body.contains(' ')) {
393 let mut params = crate::form_data::parse_form_data(body);
394
395 if params.contains_key("password") {
396 params.insert("password".to_string(), password.clone());
397 *body = crate::form_data::encode_form_data(¶ms);
398 }
399 }
400 }
401 })
402 .await?;
403
404 log::debug!("โ
Test password set in cassette");
405 Ok(())
406}
407
408pub async fn extract_username_from_cassette<P: Into<PathBuf>>(
411 cassette_path: P,
412) -> Result<Option<String>, Error> {
413 let path = cassette_path.into();
414 let cassette = Cassette::load_from_file(path).await?;
415
416 for interaction in &cassette.interactions {
417 if let Some(body) = &interaction.request.body {
418 if body.contains('=') && (body.contains('&') || !body.contains(' ')) {
419 let params = crate::form_data::parse_form_data(body);
420
421 let username_fields = ["username", "username_or_email", "user", "email"];
423 for field in &username_fields {
424 if let Some(username) = params.get(*field) {
425 if !username.starts_with("[FILTERED") && !username.starts_with("[SANITIZED")
427 {
428 return Ok(Some(username.clone()));
429 }
430 }
431 }
432 }
433 }
434 }
435
436 Ok(None)
437}
438
439#[derive(Debug)]
440pub struct CassetteAnalysis {
441 pub file_path: PathBuf,
442 pub total_interactions: usize,
443 pub requests_with_form_data: Vec<usize>,
444 pub requests_with_credentials: Vec<(usize, Vec<(String, String)>)>,
445 pub sensitive_headers: Vec<(usize, String, Vec<String>)>,
446}
447
448impl CassetteAnalysis {
449 pub fn print_report(&self) {
451 log::debug!("๐ Cassette Analysis Report");
452 log::debug!("=====================================");
453 log::debug!("File: {:?}", self.file_path);
454 log::debug!("Total interactions: {}", self.total_interactions);
455 log::debug!("");
456
457 if !self.requests_with_form_data.is_empty() {
458 log::debug!(
459 "๐ Interactions with form data: {}",
460 self.requests_with_form_data.len()
461 );
462 for idx in &self.requests_with_form_data {
463 log::debug!(" - Interaction #{idx}");
464 }
465 log::debug!("");
466 }
467
468 if !self.requests_with_credentials.is_empty() {
469 log::debug!(
470 "๐ Interactions containing credentials: {}",
471 self.requests_with_credentials.len()
472 );
473 for (idx, credentials) in &self.requests_with_credentials {
474 log::debug!(
475 " - Interaction #{}: {} credential fields",
476 idx,
477 credentials.len()
478 );
479 for (key, value) in credentials {
480 let preview = if value.len() > 20 {
481 format!("{}...", &value[..20])
482 } else {
483 value.clone()
484 };
485 log::debug!(" * {key}: {preview}");
486 }
487 }
488 log::debug!("");
489 }
490
491 if !self.sensitive_headers.is_empty() {
492 log::debug!(
493 "๐ท๏ธ Interactions with sensitive headers: {}",
494 self.sensitive_headers.len()
495 );
496 for (idx, header_name, header_values) in &self.sensitive_headers {
497 log::debug!(" - Interaction #{idx}: {header_name} header");
498 for value in header_values {
499 let preview = if value.len() > 50 {
500 format!("{}...", &value[..50])
501 } else {
502 value.clone()
503 };
504 log::debug!(" * {preview}");
505 }
506 }
507 log::debug!("");
508 }
509
510 log::debug!("๐ก Recommendations:");
511 if !self.requests_with_credentials.is_empty() {
512 log::debug!(
513 " - Use SmartFormFilter to automatically detect and filter form credentials"
514 );
515 }
516 if !self.sensitive_headers.is_empty() {
517 log::debug!(" - Use HeaderFilter to filter sensitive headers like cookies and tokens");
518 }
519 if self.requests_with_form_data.is_empty() && self.sensitive_headers.is_empty() {
520 log::debug!(" - No obvious sensitive data detected, but consider reviewing manually");
521 }
522 }
523}