Skip to main content

redstr/transformations/
cloudflare.rs

1use crate::rng::SimpleRng;
2
3/// Generates Cloudflare Turnstile challenge variations.
4///
5/// Cloudflare Turnstile is a privacy-focused CAPTCHA alternative that uses
6/// challenge-response mechanisms. This function generates variations of
7/// Turnstile challenge tokens and responses for bot detection evasion testing.
8///
9/// Useful for red team Cloudflare bypass testing and blue team bot detection validation.
10///
11/// # Examples
12///
13/// ```
14/// use redstr::cloudflare_turnstile_variation;
15/// let challenge = "challenge-token";
16/// let result = cloudflare_turnstile_variation(challenge);
17/// assert!(result.len() > 0);
18/// ```
19pub fn cloudflare_turnstile_variation(input: &str) -> String {
20    let mut rng = SimpleRng::new();
21
22    // Common Turnstile token patterns
23    let patterns = [
24        format!("{}-{}", input, generate_random_suffix(&mut rng)),
25        format!("{}_{}", input, generate_random_suffix(&mut rng)),
26        format!("cf-turnstile-{}", input),
27        format!("turnstile-{}-{}", input, generate_random_suffix(&mut rng)),
28    ];
29
30    patterns[rng.next() as usize % patterns.len()].clone()
31}
32
33/// Generates Cloudflare challenge response patterns.
34///
35/// Cloudflare uses various challenge mechanisms (Turnstile, __cf_bm cookies, etc.).
36/// This function generates response patterns that mimic legitimate challenge responses.
37///
38/// Useful for testing Cloudflare challenge bypass techniques and bot detection evasion.
39///
40/// # Examples
41///
42/// ```
43/// use redstr::cloudflare_challenge_response;
44/// let challenge = "cf_clearance=abc123";
45/// let result = cloudflare_challenge_response(challenge);
46/// assert!(result.len() > 0);
47/// ```
48pub fn cloudflare_challenge_response(input: &str) -> String {
49    let mut rng = SimpleRng::new();
50
51    // Generate response variations for Cloudflare challenges
52    if input.contains("cf_clearance") || input.contains("__cf_bm") {
53        // Cookie-based challenge response
54        input
55            .chars()
56            .map(|c| {
57                match rng.next() % 10 {
58                    0..=6 => c.to_string(),
59                    7 => {
60                        // Occasionally add spacing variations
61                        if c == '=' {
62                            if rng.next() % 2 == 0 {
63                                " = ".to_string()
64                            } else {
65                                "=".to_string()
66                            }
67                        } else {
68                            c.to_string()
69                        }
70                    }
71                    8 => {
72                        // Underscore/hyphen variations
73                        if c == '_' && rng.next() % 3 == 0 {
74                            "-".to_string()
75                        } else if c == '-' && rng.next() % 3 == 0 {
76                            "_".to_string()
77                        } else {
78                            c.to_string()
79                        }
80                    }
81                    _ => c.to_string(),
82                }
83            })
84            .collect()
85    } else if input.contains("turnstile") || input.contains("challenge") {
86        // Turnstile challenge response
87        let mut result = input.to_string();
88        if rng.next() % 2 == 0 {
89            result.push_str(&format!("-{}", generate_random_suffix(&mut rng)));
90        }
91        result
92    } else {
93        // Generic challenge response - add timestamp-like suffix
94        format!("{}-{}", input, generate_random_suffix(&mut rng))
95    }
96}
97
98/// Generates TLS handshake pattern variations for Cloudflare bot detection evasion.
99///
100/// Cloudflare analyzes TLS handshake characteristics (cipher suites, extensions, etc.)
101/// to fingerprint clients. This function generates variations of TLS handshake patterns.
102///
103/// Useful for red team TLS fingerprinting evasion and blue team detection testing.
104///
105/// # Examples
106///
107/// ```
108/// use redstr::tls_handshake_pattern;
109/// let pattern = "TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256";
110/// let result = tls_handshake_pattern(pattern);
111/// assert!(result.len() > 0);
112/// ```
113pub fn tls_handshake_pattern(input: &str) -> String {
114    let mut rng = SimpleRng::new();
115
116    // Common TLS handshake pattern variations
117    let separators = [":", ",", " ", "-"];
118    let separator = separators[rng.next() as usize % separators.len()];
119
120    input
121        .split(|c: char| [':', ',', ' ', '-'].contains(&c))
122        .enumerate()
123        .map(|(i, part)| {
124            if i > 0 {
125                format!("{}{}", separator, part)
126            } else {
127                part.to_string()
128            }
129        })
130        .collect::<Vec<_>>()
131        .join("")
132}
133
134/// Generates canvas fingerprint variations for Cloudflare bot detection evasion.
135///
136/// Canvas fingerprinting is a technique used to identify browsers based on
137/// how they render canvas elements. This function generates variations that
138/// might affect canvas fingerprinting results.
139///
140/// Useful for red team browser fingerprinting evasion and blue team detection testing.
141///
142/// # Examples
143///
144/// ```
145/// use redstr::canvas_fingerprint_variation;
146/// let canvas_data = "canvas-fingerprint-data";
147/// let result = canvas_fingerprint_variation(canvas_data);
148/// assert!(result.len() > 0);
149/// ```
150pub fn canvas_fingerprint_variation(input: &str) -> String {
151    let mut rng = SimpleRng::new();
152
153    // Canvas fingerprint variations typically involve subtle rendering differences
154    // This function generates string variations that might affect fingerprinting
155    input
156        .chars()
157        .map(|c| {
158            match rng.next() % 15 {
159                0..=11 => c.to_string(),
160                12 => {
161                    // Occasionally swap similar characters
162                    match c {
163                        '0' => {
164                            if rng.next() % 2 == 0 {
165                                "O".to_string()
166                            } else {
167                                "0".to_string()
168                            }
169                        }
170                        '1' => {
171                            if rng.next() % 2 == 0 {
172                                "l".to_string()
173                            } else {
174                                "1".to_string()
175                            }
176                        }
177                        'O' => {
178                            if rng.next() % 2 == 0 {
179                                "0".to_string()
180                            } else {
181                                "O".to_string()
182                            }
183                        }
184                        'l' => {
185                            if rng.next() % 2 == 0 {
186                                "1".to_string()
187                            } else {
188                                "l".to_string()
189                            }
190                        }
191                        _ => c.to_string(),
192                    }
193                }
194                13 => {
195                    // Add subtle spacing variations
196                    if c.is_whitespace() && rng.next() % 2 == 0 {
197                        "  ".to_string() // Double space
198                    } else {
199                        c.to_string()
200                    }
201                }
202                _ => c.to_string(),
203            }
204        })
205        .collect()
206}
207
208/// Obfuscates WebGL fingerprint data for Cloudflare bot detection evasion.
209///
210/// WebGL fingerprinting uses graphics rendering characteristics to identify browsers.
211/// This function obfuscates WebGL-related strings that might be used in fingerprinting.
212///
213/// Useful for red team WebGL fingerprinting evasion and blue team detection testing.
214///
215/// # Examples
216///
217/// ```
218/// use redstr::webgl_fingerprint_obfuscate;
219/// let webgl_data = "WebGL 2.0 Renderer: ANGLE (Intel, Intel(R) UHD Graphics 620 Direct3D11 vs_5_0 ps_5_0)";
220/// let result = webgl_fingerprint_obfuscate(webgl_data);
221/// assert!(result.len() > 0);
222/// ```
223pub fn webgl_fingerprint_obfuscate(input: &str) -> String {
224    let mut rng = SimpleRng::new();
225
226    // WebGL fingerprint obfuscation - vary version numbers, renderer names, etc.
227    input
228        .chars()
229        .map(|c| {
230            match rng.next() % 12 {
231                0..=9 => c.to_string(),
232                10 => {
233                    // Vary version numbers slightly
234                    if c.is_ascii_digit() && rng.next() % 4 == 0 {
235                        // Occasionally change digit
236                        let digit = c.to_digit(10).unwrap();
237                        let new_digit = (digit + 1) % 10;
238                        char::from_digit(new_digit, 10).unwrap_or(c).to_string()
239                    } else {
240                        c.to_string()
241                    }
242                }
243                _ => {
244                    // Case variations for version strings
245                    if c == '.' && rng.next() % 3 == 0 {
246                        ".".to_string()
247                    } else if c.is_alphabetic() && rng.next() % 5 == 0 {
248                        if c.is_uppercase() {
249                            c.to_lowercase().to_string()
250                        } else {
251                            c.to_uppercase().to_string()
252                        }
253                    } else {
254                        c.to_string()
255                    }
256                }
257            }
258        })
259        .collect()
260}
261
262/// Generates font fingerprint consistency variations for Cloudflare bot detection evasion.
263///
264/// Font fingerprinting identifies browsers by checking which fonts are available.
265/// This function generates variations that maintain consistency while evading detection.
266///
267/// Useful for red team font fingerprinting evasion and blue team detection testing.
268///
269/// # Examples
270///
271/// ```
272/// use redstr::font_fingerprint_consistency;
273/// let font_list = "Arial, Helvetica, Times New Roman";
274/// let result = font_fingerprint_consistency(font_list);
275/// assert!(result.len() > 0);
276/// ```
277pub fn font_fingerprint_consistency(input: &str) -> String {
278    let mut rng = SimpleRng::new();
279
280    // Handle empty input
281    if input.is_empty() {
282        return input.to_string();
283    }
284
285    // Font list variations - maintain valid font names but vary formatting
286    let fonts: Vec<&str> = input
287        .split(|c: char| [',', ';'].contains(&c))
288        .map(|s| s.trim())
289        .filter(|s| !s.is_empty())
290        .collect();
291
292    if fonts.is_empty() {
293        return input.to_string();
294    }
295
296    let separators = [", ", ",", "; ", ";"];
297    let separator = separators[rng.next() as usize % separators.len()];
298
299    fonts
300        .iter()
301        .enumerate()
302        .map(|(i, font)| {
303            let mut font_str = font.to_string();
304            // Occasionally add/remove quotes
305            if rng.next() % 4 == 0 && !font_str.starts_with('"') {
306                font_str = format!("\"{}\"", font_str);
307            }
308            if i > 0 {
309                format!("{}{}", separator, font_str)
310            } else {
311                font_str
312            }
313        })
314        .collect::<Vec<_>>()
315        .join("")
316}
317
318/// Helper function to generate random suffix for tokens
319fn generate_random_suffix(rng: &mut SimpleRng) -> String {
320    let chars: Vec<char> = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
321        .chars()
322        .collect();
323    let length = 8 + (rng.next() % 8) as usize; // 8-15 characters
324    (0..length)
325        .map(|_| chars[rng.next() as usize % chars.len()])
326        .collect()
327}
328
329#[cfg(test)]
330mod tests {
331    use super::*;
332
333    #[test]
334    fn test_cloudflare_turnstile_variation() {
335        let challenge = "challenge-token";
336        let result = cloudflare_turnstile_variation(challenge);
337        assert!(result.len() > challenge.len());
338        assert!(result.contains(challenge));
339    }
340
341    #[test]
342    fn test_cloudflare_challenge_response() {
343        let challenge = "cf_clearance=abc123";
344        let result = cloudflare_challenge_response(challenge);
345        assert!(result.len() > 0);
346        // Should preserve key parts of the challenge
347        assert!(
348            result.to_lowercase().contains("cf_clearance")
349                || result.to_lowercase().contains("cf-clearance")
350        );
351    }
352
353    #[test]
354    fn test_tls_handshake_pattern() {
355        let pattern = "TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256";
356        let result = tls_handshake_pattern(pattern);
357        assert!(result.len() > 0);
358        assert!(result.contains("TLS"));
359    }
360
361    #[test]
362    fn test_canvas_fingerprint_variation() {
363        let canvas_data = "canvas-fingerprint-data";
364        let result = canvas_fingerprint_variation(canvas_data);
365        assert!(result.len() > 0);
366        assert!(result.contains("canvas"));
367    }
368
369    #[test]
370    fn test_webgl_fingerprint_obfuscate() {
371        let webgl_data = "WebGL 2.0 Renderer: ANGLE";
372        let result = webgl_fingerprint_obfuscate(webgl_data);
373        assert!(result.len() > 0);
374        assert!(result.to_lowercase().contains("webgl"));
375    }
376
377    #[test]
378    fn test_font_fingerprint_consistency() {
379        let font_list = "Arial, Helvetica, Times New Roman";
380        let result = font_fingerprint_consistency(font_list);
381        assert!(result.len() > 0);
382        assert!(result.to_lowercase().contains("arial"));
383    }
384
385    #[test]
386    fn test_cloudflare_challenge_response_turnstile() {
387        let challenge = "turnstile-challenge-123";
388        let result = cloudflare_challenge_response(challenge);
389        assert!(result.len() > 0);
390        assert!(result.contains("turnstile") || result.contains("challenge"));
391    }
392
393    #[test]
394    fn test_cloudflare_challenge_response_generic() {
395        let challenge = "generic-challenge";
396        let result = cloudflare_challenge_response(challenge);
397        assert!(result.len() > 0);
398        // Generic challenge should contain the original challenge
399        assert!(result.contains(challenge));
400    }
401
402    #[test]
403    fn test_cloudflare_turnstile_variation_empty_string() {
404        let result = cloudflare_turnstile_variation("");
405        assert!(result.len() > 0);
406    }
407
408    #[test]
409    fn test_cloudflare_turnstile_variation_unicode() {
410        let challenge = "challenge-测试-токен";
411        let result = cloudflare_turnstile_variation(challenge);
412        assert!(result.contains(challenge));
413    }
414
415    #[test]
416    fn test_cloudflare_challenge_response_cf_bm() {
417        let challenge = "__cf_bm=cookie123";
418        let result = cloudflare_challenge_response(challenge);
419        assert!(result.len() > 0);
420        assert!(result.to_lowercase().contains("cf_bm") || result.to_lowercase().contains("cf-bm"));
421    }
422
423    #[test]
424    fn test_cloudflare_challenge_response_empty_string() {
425        let result = cloudflare_challenge_response("");
426        // Empty string should return a non-empty result (adds suffix)
427        assert!(result.len() > 0);
428    }
429
430    #[test]
431    fn test_tls_handshake_pattern_empty_string() {
432        let result = tls_handshake_pattern("");
433        assert_eq!(result, "");
434    }
435
436    #[test]
437    fn test_tls_handshake_pattern_single_cipher() {
438        let pattern = "TLS_AES_256_GCM_SHA384";
439        let result = tls_handshake_pattern(pattern);
440        assert!(result.contains("TLS"));
441    }
442
443    #[test]
444    fn test_tls_handshake_pattern_comma_separated() {
445        let pattern = "TLS_AES_256_GCM_SHA384,TLS_CHACHA20_POLY1305_SHA256";
446        let result = tls_handshake_pattern(pattern);
447        assert!(result.contains("TLS"));
448    }
449
450    #[test]
451    fn test_tls_handshake_pattern_space_separated() {
452        let pattern = "TLS_AES_256_GCM_SHA384 TLS_CHACHA20_POLY1305_SHA256";
453        let result = tls_handshake_pattern(pattern);
454        assert!(result.contains("TLS"));
455    }
456
457    #[test]
458    fn test_canvas_fingerprint_variation_empty_string() {
459        let result = canvas_fingerprint_variation("");
460        assert_eq!(result, "");
461    }
462
463    #[test]
464    fn test_canvas_fingerprint_variation_unicode() {
465        let canvas_data = "canvas-测试-data";
466        let result = canvas_fingerprint_variation(canvas_data);
467        assert!(result.len() > 0);
468    }
469
470    #[test]
471    fn test_canvas_fingerprint_variation_preserves_length_approximately() {
472        let canvas_data = "test123";
473        let result = canvas_fingerprint_variation(canvas_data);
474        // Length should be similar (may vary slightly due to character swaps)
475        assert!(result.len() >= canvas_data.len() - 2);
476        assert!(result.len() <= canvas_data.len() + 2);
477    }
478
479    #[test]
480    fn test_webgl_fingerprint_obfuscate_empty_string() {
481        let result = webgl_fingerprint_obfuscate("");
482        assert_eq!(result, "");
483    }
484
485    #[test]
486    fn test_webgl_fingerprint_obfuscate_with_version() {
487        let webgl_data = "WebGL 2.0";
488        let result = webgl_fingerprint_obfuscate(webgl_data);
489        assert!(result.len() > 0);
490        assert!(result.to_lowercase().contains("webgl"));
491    }
492
493    #[test]
494    fn test_webgl_fingerprint_obfuscate_preserves_structure() {
495        let webgl_data = "WebGL 2.0 Renderer: ANGLE";
496        let result = webgl_fingerprint_obfuscate(webgl_data);
497        // Should preserve overall structure
498        assert!(result.to_lowercase().contains("webgl"));
499        assert!(result.to_lowercase().contains("renderer"));
500    }
501
502    #[test]
503    fn test_font_fingerprint_consistency_empty_string() {
504        let result = font_fingerprint_consistency("");
505        assert_eq!(result, "");
506    }
507
508    #[test]
509    fn test_font_fingerprint_consistency_single_font() {
510        let font_list = "Arial";
511        let result = font_fingerprint_consistency(font_list);
512        assert!(result.len() > 0);
513        assert!(result.to_lowercase().contains("arial"));
514    }
515
516    #[test]
517    fn test_font_fingerprint_consistency_semicolon_separated() {
518        let font_list = "Arial; Helvetica; Times New Roman";
519        let result = font_fingerprint_consistency(font_list);
520        assert!(result.len() > 0);
521        assert!(result.to_lowercase().contains("arial"));
522    }
523
524    #[test]
525    fn test_font_fingerprint_consistency_preserves_fonts() {
526        let font_list = "Arial, Helvetica, Times New Roman";
527        let result = font_fingerprint_consistency(font_list);
528        // All fonts should be present
529        assert!(result.to_lowercase().contains("arial"));
530        assert!(result.to_lowercase().contains("helvetica"));
531        assert!(result.to_lowercase().contains("times"));
532    }
533
534    #[test]
535    fn test_cloudflare_turnstile_variation_multiple_calls() {
536        let challenge = "test-challenge";
537        let results: Vec<String> = (0..10)
538            .map(|_| cloudflare_turnstile_variation(challenge))
539            .collect();
540        // All results should contain the challenge
541        for result in &results {
542            assert!(result.contains(challenge));
543        }
544        // At least some results should differ (randomness)
545        let unique_results: std::collections::HashSet<&String> = results.iter().collect();
546        // Due to randomness, we might get some duplicates, but not all should be identical
547        assert!(unique_results.len() >= 1);
548    }
549
550    #[test]
551    fn test_cloudflare_challenge_response_special_characters() {
552        let challenge = "cf_clearance=abc!@#$%^&*()123";
553        let result = cloudflare_challenge_response(challenge);
554        assert!(result.len() > 0);
555        assert!(
556            result.to_lowercase().contains("cf_clearance")
557                || result.to_lowercase().contains("cf-clearance")
558        );
559    }
560
561    #[test]
562    fn test_tls_handshake_pattern_preserves_ciphers() {
563        let pattern = "TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256";
564        let result = tls_handshake_pattern(pattern);
565        // Should contain both cipher names
566        assert!(result.contains("TLS_AES_256_GCM_SHA384"));
567        assert!(result.contains("TLS_CHACHA20_POLY1305_SHA256"));
568    }
569
570    #[test]
571    fn test_cloudflare_turnstile_variation_long_string() {
572        let challenge = "a".repeat(1000);
573        let result = cloudflare_turnstile_variation(&challenge);
574        assert!(result.len() > challenge.len());
575        assert!(result.contains(&challenge));
576    }
577
578    #[test]
579    fn test_cloudflare_challenge_response_long_string() {
580        let challenge = "cf_clearance=".to_string() + &"a".repeat(500);
581        let result = cloudflare_challenge_response(&challenge);
582        assert!(result.len() > 0);
583        assert!(
584            result.to_lowercase().contains("cf_clearance")
585                || result.to_lowercase().contains("cf-clearance")
586        );
587    }
588
589    #[test]
590    fn test_tls_handshake_pattern_long_list() {
591        let pattern = (0..20)
592            .map(|i| format!("TLS_CIPHER_{}", i))
593            .collect::<Vec<_>>()
594            .join(":");
595        let result = tls_handshake_pattern(&pattern);
596        assert!(result.len() > 0);
597        assert!(result.contains("TLS_CIPHER_0"));
598        assert!(result.contains("TLS_CIPHER_19"));
599    }
600
601    #[test]
602    fn test_canvas_fingerprint_variation_special_chars_only() {
603        let canvas_data = "!@#$%^&*()";
604        let result = canvas_fingerprint_variation(canvas_data);
605        assert!(result.len() > 0);
606    }
607
608    #[test]
609    fn test_webgl_fingerprint_obfuscate_long_string() {
610        let webgl_data = "WebGL ".to_string() + &"2.0 ".repeat(100);
611        let result = webgl_fingerprint_obfuscate(&webgl_data);
612        assert!(result.len() > 0);
613        assert!(result.to_lowercase().contains("webgl"));
614    }
615
616    #[test]
617    fn test_font_fingerprint_consistency_long_list() {
618        let fonts: Vec<String> = (0..50).map(|i| format!("Font{}", i)).collect();
619        let font_list = fonts.join(", ");
620        let result = font_fingerprint_consistency(&font_list);
621        assert!(result.len() > 0);
622        assert!(result.to_lowercase().contains("font0"));
623        assert!(result.to_lowercase().contains("font49"));
624    }
625
626    #[test]
627    fn test_cloudflare_turnstile_variation_randomness() {
628        let challenge = "test-challenge";
629        let mut results = std::collections::HashSet::new();
630        // Run multiple times to check for variation
631        for _ in 0..50 {
632            results.insert(cloudflare_turnstile_variation(challenge));
633        }
634        // Should have some variation (not all identical)
635        // Due to randomness, we expect at least a few different results
636        assert!(results.len() >= 1);
637    }
638
639    #[test]
640    fn test_cloudflare_challenge_response_randomness() {
641        let challenge = "cf_clearance=test123";
642        let mut results = std::collections::HashSet::new();
643        for _ in 0..50 {
644            results.insert(cloudflare_challenge_response(challenge));
645        }
646        // Should have some variation
647        assert!(results.len() >= 1);
648    }
649
650    #[test]
651    fn test_tls_handshake_pattern_variation() {
652        let pattern = "TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256";
653        let mut results = std::collections::HashSet::new();
654        for _ in 0..20 {
655            results.insert(tls_handshake_pattern(pattern));
656        }
657        // Should have some variation in separators
658        assert!(results.len() >= 1);
659    }
660
661    #[test]
662    fn test_font_fingerprint_consistency_variation() {
663        let font_list = "Arial, Helvetica";
664        let mut results = std::collections::HashSet::new();
665        for _ in 0..20 {
666            results.insert(font_fingerprint_consistency(font_list));
667        }
668        // Should have some variation in formatting
669        assert!(results.len() >= 1);
670    }
671
672    #[test]
673    fn test_cloudflare_functions_preserve_non_empty_output() {
674        let test_cases = vec![
675            "test",
676            "challenge-token-123",
677            "cf_clearance=abc",
678            "TLS_AES_256_GCM_SHA384",
679            "canvas-data",
680            "WebGL 2.0",
681            "Arial, Helvetica",
682        ];
683
684        for input in test_cases {
685            assert!(
686                cloudflare_turnstile_variation(input).len() > 0,
687                "turnstile failed for: {}",
688                input
689            );
690            assert!(
691                cloudflare_challenge_response(input).len() > 0,
692                "challenge_response failed for: {}",
693                input
694            );
695            // These functions can return empty strings for empty input, so just check they don't panic
696            let _ = tls_handshake_pattern(input);
697            let _ = canvas_fingerprint_variation(input);
698            let _ = webgl_fingerprint_obfuscate(input);
699            let _ = font_fingerprint_consistency(input);
700        }
701    }
702
703    #[test]
704    fn test_cloudflare_challenge_response_all_cookie_formats() {
705        let formats = vec![
706            "cf_clearance=token123",
707            "__cf_bm=cookie456",
708            "cf-clearance=token789",
709            "__cf-bm=cookie012",
710        ];
711
712        for format in formats {
713            let result = cloudflare_challenge_response(format);
714            assert!(result.len() > 0, "Failed for format: {}", format);
715            // Should preserve the cookie name in some form
716            let lower = result.to_lowercase();
717            assert!(
718                lower.contains("cf") || lower.contains("clearance") || lower.contains("bm"),
719                "Result doesn't contain cookie identifier: {}",
720                result
721            );
722        }
723    }
724}