Skip to main content

mockforge_bench/
wafbench.rs

1//! WAFBench YAML parser for importing CRS (Core Rule Set) attack patterns
2//!
3//! This module parses WAFBench YAML test files from the Microsoft WAFBench project
4//! (<https://github.com/microsoft/WAFBench>) and converts them into security test payloads
5//! compatible with MockForge's security testing framework.
6//!
7//! # WAFBench YAML Format
8//!
9//! WAFBench test files follow this structure:
10//! ```yaml
11//! meta:
12//!   author: "author-name"
13//!   description: "Tests for rule XXXXXX"
14//!   enabled: true
15//!   name: "XXXXXX.yaml"
16//!
17//! tests:
18//!   - desc: "Attack scenario description"
19//!     test_title: "XXXXXX-N"
20//!     stages:
21//!       - input:
22//!           dest_addr: "127.0.0.1"
23//!           headers:
24//!             Host: "localhost"
25//!             User-Agent: "Mozilla/5.0"
26//!           method: "GET"
27//!           port: 80
28//!           uri: "/path?param=<script>alert(1)</script>"
29//!         output:
30//!           status: [200, 403, 404]
31//! ```
32//!
33//! # Usage
34//!
35//! ```bash
36//! mockforge bench spec.yaml --wafbench-dir ./wafbench/REQUEST-941-*
37//! ```
38
39use crate::error::{BenchError, Result};
40use crate::security_payloads::{
41    PayloadLocation as SecurityPayloadLocation, SecurityCategory, SecurityPayload,
42};
43use glob::glob;
44use serde::{Deserialize, Serialize};
45use std::collections::HashMap;
46use std::path::Path;
47
48/// WAFBench test file metadata
49#[derive(Debug, Clone, Deserialize, Serialize)]
50pub struct WafBenchMeta {
51    /// Author of the test file
52    pub author: Option<String>,
53    /// Description of what the tests cover
54    pub description: Option<String>,
55    /// Whether the tests are enabled
56    #[serde(default = "default_enabled")]
57    pub enabled: bool,
58    /// Name of the test file
59    pub name: Option<String>,
60}
61
62fn default_enabled() -> bool {
63    true
64}
65
66/// A single WAFBench test case
67#[derive(Debug, Clone, Deserialize, Serialize)]
68pub struct WafBenchTest {
69    /// Description of the attack scenario
70    pub desc: Option<String>,
71    /// Unique test identifier (e.g., "941100-1")
72    pub test_title: String,
73    /// Test stages (request/response pairs)
74    #[serde(default)]
75    pub stages: Vec<WafBenchStage>,
76}
77
78/// A test stage containing input (request) and expected output (response)
79/// Supports both direct format and CRS v3.3 format with nested `stage:` wrapper
80#[derive(Debug, Clone, Deserialize, Serialize)]
81pub struct WafBenchStage {
82    /// The request configuration (direct format)
83    pub input: Option<WafBenchInput>,
84    /// Expected response (direct format)
85    pub output: Option<WafBenchOutput>,
86    /// Nested stage for CRS v3.3 format (stage: { input: ..., output: ... })
87    pub stage: Option<WafBenchStageInner>,
88}
89
90/// Inner stage structure for CRS v3.3 format
91#[derive(Debug, Clone, Deserialize, Serialize)]
92pub struct WafBenchStageInner {
93    /// The request configuration
94    pub input: WafBenchInput,
95    /// Expected response
96    pub output: Option<WafBenchOutput>,
97}
98
99impl WafBenchStage {
100    /// Get the input from either direct or nested format
101    pub fn get_input(&self) -> Option<&WafBenchInput> {
102        // Prefer nested stage format (CRS v3.3), fall back to direct format
103        if let Some(stage) = &self.stage {
104            Some(&stage.input)
105        } else {
106            self.input.as_ref()
107        }
108    }
109
110    /// Get the output from either direct or nested format
111    pub fn get_output(&self) -> Option<&WafBenchOutput> {
112        // Prefer nested stage format (CRS v3.3), fall back to direct format
113        if let Some(stage) = &self.stage {
114            stage.output.as_ref()
115        } else {
116            self.output.as_ref()
117        }
118    }
119}
120
121/// Request configuration for a WAFBench test
122#[derive(Debug, Clone, Deserialize, Serialize)]
123pub struct WafBenchInput {
124    /// Target address
125    pub dest_addr: Option<String>,
126    /// HTTP headers
127    #[serde(default)]
128    pub headers: HashMap<String, String>,
129    /// HTTP method
130    #[serde(default = "default_method")]
131    pub method: String,
132    /// Target port
133    #[serde(default = "default_port")]
134    pub port: u16,
135    /// Request URI (may contain attack payloads)
136    pub uri: Option<String>,
137    /// Request body data
138    pub data: Option<String>,
139    /// Protocol version
140    pub version: Option<String>,
141}
142
143fn default_method() -> String {
144    "GET".to_string()
145}
146
147fn default_port() -> u16 {
148    80
149}
150
151/// Expected response for a WAFBench test
152#[derive(Debug, Clone, Deserialize, Serialize)]
153pub struct WafBenchOutput {
154    /// Expected HTTP status codes (any match is valid)
155    #[serde(default)]
156    pub status: Vec<u16>,
157    /// Expected response headers
158    #[serde(default)]
159    pub response_headers: HashMap<String, String>,
160    /// Log contains patterns (can be string or array in different formats)
161    #[serde(default, deserialize_with = "deserialize_string_or_vec")]
162    pub log_contains: Vec<String>,
163    /// Log does not contain patterns (can be string or array in different formats)
164    #[serde(default, deserialize_with = "deserialize_string_or_vec")]
165    pub no_log_contains: Vec<String>,
166}
167
168/// Deserialize a field that can be either a single string or a Vec of strings
169fn deserialize_string_or_vec<'de, D>(deserializer: D) -> std::result::Result<Vec<String>, D::Error>
170where
171    D: serde::Deserializer<'de>,
172{
173    use serde::de::{self, Visitor};
174
175    struct StringOrVec;
176
177    impl<'de> Visitor<'de> for StringOrVec {
178        type Value = Vec<String>;
179
180        fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
181            formatter.write_str("string or array of strings")
182        }
183
184        fn visit_str<E>(self, value: &str) -> std::result::Result<Self::Value, E>
185        where
186            E: de::Error,
187        {
188            Ok(vec![value.to_string()])
189        }
190
191        fn visit_string<E>(self, value: String) -> std::result::Result<Self::Value, E>
192        where
193            E: de::Error,
194        {
195            Ok(vec![value])
196        }
197
198        fn visit_seq<A>(self, mut seq: A) -> std::result::Result<Self::Value, A::Error>
199        where
200            A: de::SeqAccess<'de>,
201        {
202            let mut vec = Vec::new();
203            while let Some(value) = seq.next_element::<String>()? {
204                vec.push(value);
205            }
206            Ok(vec)
207        }
208
209        fn visit_none<E>(self) -> std::result::Result<Self::Value, E>
210        where
211            E: de::Error,
212        {
213            Ok(Vec::new())
214        }
215
216        fn visit_unit<E>(self) -> std::result::Result<Self::Value, E>
217        where
218            E: de::Error,
219        {
220            Ok(Vec::new())
221        }
222    }
223
224    deserializer.deserialize_any(StringOrVec)
225}
226
227/// Complete WAFBench test file structure
228#[derive(Debug, Clone, Deserialize, Serialize)]
229pub struct WafBenchFile {
230    /// Test file metadata
231    pub meta: WafBenchMeta,
232    /// Test cases
233    #[serde(default)]
234    pub tests: Vec<WafBenchTest>,
235}
236
237/// A parsed WAFBench test case ready for use in security testing
238#[derive(Debug, Clone)]
239pub struct WafBenchTestCase {
240    /// Test identifier
241    pub test_id: String,
242    /// Description
243    pub description: String,
244    /// CRS rule ID (e.g., 941100)
245    pub rule_id: String,
246    /// Security category
247    pub category: SecurityCategory,
248    /// HTTP method
249    pub method: String,
250    /// Attack payloads extracted from the test
251    pub payloads: Vec<WafBenchPayload>,
252    /// Expected to be blocked (403)
253    pub expects_block: bool,
254}
255
256/// A specific payload from a WAFBench test
257#[derive(Debug, Clone)]
258pub struct WafBenchPayload {
259    /// The payload location (uri, header, body)
260    pub location: PayloadLocation,
261    /// The actual payload string
262    pub value: String,
263    /// Header name if location is Header
264    pub header_name: Option<String>,
265}
266
267/// Where the payload is injected
268#[derive(Debug, Clone, Copy, PartialEq, Eq)]
269pub enum PayloadLocation {
270    /// Payload in URI/query string
271    Uri,
272    /// Payload in HTTP header
273    Header,
274    /// Payload in request body
275    Body,
276}
277
278impl std::fmt::Display for PayloadLocation {
279    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
280        match self {
281            Self::Uri => write!(f, "uri"),
282            Self::Header => write!(f, "header"),
283            Self::Body => write!(f, "body"),
284        }
285    }
286}
287
288/// WAFBench loader and parser
289pub struct WafBenchLoader {
290    /// Loaded test cases
291    test_cases: Vec<WafBenchTestCase>,
292    /// Statistics
293    stats: WafBenchStats,
294}
295
296/// Statistics about loaded WAFBench tests
297#[derive(Debug, Clone, Default)]
298pub struct WafBenchStats {
299    /// Number of files processed
300    pub files_processed: usize,
301    /// Number of test cases loaded
302    pub test_cases_loaded: usize,
303    /// Number of payloads extracted
304    pub payloads_extracted: usize,
305    /// Tests by category
306    pub by_category: HashMap<SecurityCategory, usize>,
307    /// Files that failed to parse
308    pub parse_errors: Vec<String>,
309}
310
311impl WafBenchLoader {
312    /// Create a new empty loader
313    pub fn new() -> Self {
314        Self {
315            test_cases: Vec::new(),
316            stats: WafBenchStats::default(),
317        }
318    }
319
320    /// Load WAFBench tests from a directory pattern (supports glob)
321    ///
322    /// # Arguments
323    /// * `pattern` - Glob pattern like `./wafbench/REQUEST-941-*` or a direct path
324    ///
325    /// # Example
326    /// ```ignore
327    /// let loader = WafBenchLoader::new();
328    /// loader.load_from_pattern("./wafbench/REQUEST-941-APPLICATION-ATTACK-XSS/**/*.yaml")?;
329    /// ```
330    pub fn load_from_pattern(&mut self, pattern: &str) -> Result<()> {
331        // If pattern doesn't contain wildcards, check if it's a file or directory
332        if !pattern.contains('*') && !pattern.contains('?') {
333            let path = Path::new(pattern);
334            if path.is_file() {
335                // Load single file directly
336                return self.load_file(path);
337            } else if path.is_dir() {
338                return self.load_from_directory(path);
339            } else {
340                return Err(BenchError::Other(format!(
341                    "WAFBench path does not exist: {}",
342                    pattern
343                )));
344            }
345        }
346
347        // Use glob to find matching files
348        let entries = glob(pattern).map_err(|e| {
349            BenchError::Other(format!("Invalid WAFBench pattern '{}': {}", pattern, e))
350        })?;
351
352        for entry in entries {
353            match entry {
354                Ok(path) => {
355                    if path.is_file()
356                        && path.extension().is_some_and(|ext| ext == "yaml" || ext == "yml")
357                    {
358                        if let Err(e) = self.load_file(&path) {
359                            self.stats.parse_errors.push(format!("{}: {}", path.display(), e));
360                        }
361                    } else if path.is_dir() {
362                        if let Err(e) = self.load_from_directory(&path) {
363                            self.stats.parse_errors.push(format!("{}: {}", path.display(), e));
364                        }
365                    }
366                }
367                Err(e) => {
368                    self.stats.parse_errors.push(format!("Glob error: {}", e));
369                }
370            }
371        }
372
373        Ok(())
374    }
375
376    /// Load WAFBench tests from a directory (recursive)
377    pub fn load_from_directory(&mut self, dir: &Path) -> Result<()> {
378        if !dir.is_dir() {
379            return Err(BenchError::Other(format!(
380                "WAFBench path is not a directory: {}",
381                dir.display()
382            )));
383        }
384
385        self.load_directory_recursive(dir)?;
386        Ok(())
387    }
388
389    fn load_directory_recursive(&mut self, dir: &Path) -> Result<()> {
390        let entries = std::fs::read_dir(dir)
391            .map_err(|e| BenchError::Other(format!("Failed to read WAFBench directory: {}", e)))?;
392
393        for entry in entries.flatten() {
394            let path = entry.path();
395            if path.is_dir() {
396                // Recurse into subdirectories
397                self.load_directory_recursive(&path)?;
398            } else if path.extension().is_some_and(|ext| ext == "yaml" || ext == "yml") {
399                if let Err(e) = self.load_file(&path) {
400                    self.stats.parse_errors.push(format!("{}: {}", path.display(), e));
401                }
402            }
403        }
404
405        Ok(())
406    }
407
408    /// Load a single WAFBench YAML file
409    pub fn load_file(&mut self, path: &Path) -> Result<()> {
410        let content = std::fs::read_to_string(path).map_err(|e| {
411            BenchError::Other(format!("Failed to read WAFBench file {}: {}", path.display(), e))
412        })?;
413
414        let wafbench_file: WafBenchFile = serde_yaml::from_str(&content).map_err(|e| {
415            BenchError::Other(format!("Failed to parse WAFBench YAML {}: {}", path.display(), e))
416        })?;
417
418        // Skip disabled test files
419        if !wafbench_file.meta.enabled {
420            return Ok(());
421        }
422
423        self.stats.files_processed += 1;
424
425        // Determine the rule category from the file path or name
426        let category = self.detect_category(path, &wafbench_file.meta);
427
428        // Parse each test case
429        for test in wafbench_file.tests {
430            if let Some(test_case) = self.parse_test_case(&test, category) {
431                self.stats.payloads_extracted += test_case.payloads.len();
432                *self.stats.by_category.entry(category).or_insert(0) += 1;
433                self.test_cases.push(test_case);
434                self.stats.test_cases_loaded += 1;
435            }
436        }
437
438        Ok(())
439    }
440
441    /// Detect the security category from the file path
442    fn detect_category(&self, path: &Path, _meta: &WafBenchMeta) -> SecurityCategory {
443        let path_str = path.to_string_lossy().to_uppercase();
444
445        if path_str.contains("XSS") || path_str.contains("941") {
446            SecurityCategory::Xss
447        } else if path_str.contains("SQLI") || path_str.contains("942") {
448            SecurityCategory::SqlInjection
449        } else if path_str.contains("RCE") || path_str.contains("932") {
450            SecurityCategory::CommandInjection
451        } else if path_str.contains("LFI") || path_str.contains("930") {
452            SecurityCategory::PathTraversal
453        } else if path_str.contains("LDAP") {
454            SecurityCategory::LdapInjection
455        } else if path_str.contains("XXE") || path_str.contains("XML") {
456            SecurityCategory::Xxe
457        } else if path_str.contains("TEMPLATE") || path_str.contains("SSTI") {
458            SecurityCategory::Ssti
459        } else {
460            // Default to XSS as it's the most common in WAFBench
461            SecurityCategory::Xss
462        }
463    }
464
465    /// Parse a single test case into our format
466    fn parse_test_case(
467        &self,
468        test: &WafBenchTest,
469        category: SecurityCategory,
470    ) -> Option<WafBenchTestCase> {
471        // Extract rule ID from test_title (e.g., "941100-1" -> "941100")
472        let rule_id = test.test_title.split('-').next().unwrap_or(&test.test_title).to_string();
473
474        let mut payloads = Vec::new();
475        let mut method = "GET".to_string();
476        let mut expects_block = false;
477
478        for stage in &test.stages {
479            // Get input from either direct or nested format (CRS v3.3 compatibility)
480            let Some(input) = stage.get_input() else {
481                continue;
482            };
483
484            method = input.method.clone();
485
486            // Check if this test expects a block (403)
487            if let Some(output) = stage.get_output() {
488                if output.status.contains(&403) {
489                    expects_block = true;
490                }
491            }
492
493            // Extract payload from URI
494            if let Some(uri) = &input.uri {
495                // Look for attack patterns in the URI
496                if self.looks_like_attack(uri) {
497                    payloads.push(WafBenchPayload {
498                        location: PayloadLocation::Uri,
499                        value: uri.clone(),
500                        header_name: None,
501                    });
502                }
503            }
504
505            // Extract payloads from headers
506            for (header_name, header_value) in &input.headers {
507                if self.looks_like_attack(header_value) {
508                    payloads.push(WafBenchPayload {
509                        location: PayloadLocation::Header,
510                        value: header_value.clone(),
511                        header_name: Some(header_name.clone()),
512                    });
513                }
514            }
515
516            // Extract payload from body
517            if let Some(data) = &input.data {
518                if self.looks_like_attack(data) {
519                    payloads.push(WafBenchPayload {
520                        location: PayloadLocation::Body,
521                        value: data.clone(),
522                        header_name: None,
523                    });
524                }
525            }
526        }
527
528        // If no payloads found, still include the test but with full URI as payload
529        if payloads.is_empty() {
530            if let Some(stage) = test.stages.first() {
531                if let Some(input) = stage.get_input() {
532                    if let Some(uri) = &input.uri {
533                        payloads.push(WafBenchPayload {
534                            location: PayloadLocation::Uri,
535                            value: uri.clone(),
536                            header_name: None,
537                        });
538                    }
539                }
540            }
541        }
542
543        if payloads.is_empty() {
544            return None;
545        }
546
547        let description = test.desc.clone().unwrap_or_else(|| format!("CRS Rule {} test", rule_id));
548
549        Some(WafBenchTestCase {
550            test_id: test.test_title.clone(),
551            description,
552            rule_id,
553            category,
554            method,
555            payloads,
556            expects_block,
557        })
558    }
559
560    /// Check if a string looks like an attack payload
561    fn looks_like_attack(&self, s: &str) -> bool {
562        // Common attack patterns
563        let attack_patterns = [
564            "<script",
565            "javascript:",
566            "onerror=",
567            "onload=",
568            "onclick=",
569            "onfocus=",
570            "onmouseover=",
571            "eval(",
572            "alert(",
573            "document.",
574            "window.",
575            "'--",
576            "' OR ",
577            "' AND ",
578            "1=1",
579            "UNION SELECT",
580            "CONCAT(",
581            "CHAR(",
582            "../",
583            "..\\",
584            "/etc/passwd",
585            "cmd.exe",
586            "powershell",
587            "; ls",
588            "| cat",
589            "${",
590            "{{",
591            "<%",
592            "<?",
593            "<!ENTITY",
594            "SYSTEM \"",
595        ];
596
597        let lower = s.to_lowercase();
598        attack_patterns.iter().any(|p| lower.contains(&p.to_lowercase()))
599    }
600
601    /// Get all loaded test cases
602    pub fn test_cases(&self) -> &[WafBenchTestCase] {
603        &self.test_cases
604    }
605
606    /// Get statistics about loaded tests
607    pub fn stats(&self) -> &WafBenchStats {
608        &self.stats
609    }
610
611    /// Convert loaded tests to SecurityPayload format for use with existing security testing
612    pub fn to_security_payloads(&self) -> Vec<SecurityPayload> {
613        let mut payloads = Vec::new();
614
615        for test_case in &self.test_cases {
616            for payload in &test_case.payloads {
617                // Extract just the attack payload part if possible
618                let payload_str = self.extract_payload_value(&payload.value);
619
620                // Convert local PayloadLocation to SecurityPayloadLocation
621                let location = match payload.location {
622                    PayloadLocation::Uri => SecurityPayloadLocation::Uri,
623                    PayloadLocation::Header => SecurityPayloadLocation::Header,
624                    PayloadLocation::Body => SecurityPayloadLocation::Body,
625                };
626
627                let mut sec_payload = SecurityPayload::new(
628                    payload_str,
629                    test_case.category,
630                    format!(
631                        "[WAFBench {}] {} ({})",
632                        test_case.rule_id, test_case.description, payload.location
633                    ),
634                )
635                .high_risk()
636                .with_location(location);
637
638                // Add header name for header payloads
639                if let Some(header_name) = &payload.header_name {
640                    sec_payload = sec_payload.with_header_name(header_name.clone());
641                }
642
643                payloads.push(sec_payload);
644            }
645        }
646
647        payloads
648    }
649
650    /// Extract the actual attack payload from a URI or value
651    fn extract_payload_value(&self, value: &str) -> String {
652        // If it's a URI, try to extract query parameter values
653        if value.contains('?') {
654            if let Some(query) = value.split('?').nth(1) {
655                // Get the first parameter value that looks malicious
656                for param in query.split('&') {
657                    if let Some(val) = param.split('=').nth(1) {
658                        let decoded = urlencoding::decode(val).unwrap_or_else(|_| val.into());
659                        if self.looks_like_attack(&decoded) {
660                            return decoded.to_string();
661                        }
662                    }
663                }
664            }
665        }
666
667        // Return the full value if we can't extract a specific payload
668        value.to_string()
669    }
670}
671
672impl Default for WafBenchLoader {
673    fn default() -> Self {
674        Self::new()
675    }
676}
677
678#[cfg(test)]
679mod tests {
680    use super::*;
681
682    #[test]
683    fn test_parse_wafbench_yaml() {
684        let yaml = r#"
685meta:
686  author: test
687  description: Test XSS rules
688  enabled: true
689  name: test.yaml
690
691tests:
692  - desc: "XSS in URI parameter"
693    test_title: "941100-1"
694    stages:
695      - input:
696          dest_addr: "127.0.0.1"
697          headers:
698            Host: "localhost"
699            User-Agent: "Mozilla/5.0"
700          method: "GET"
701          port: 80
702          uri: "/test?param=<script>alert(1)</script>"
703        output:
704          status: [403]
705"#;
706
707        let file: WafBenchFile = serde_yaml::from_str(yaml).unwrap();
708        assert!(file.meta.enabled);
709        assert_eq!(file.tests.len(), 1);
710        assert_eq!(file.tests[0].test_title, "941100-1");
711    }
712
713    #[test]
714    fn test_detect_category() {
715        let loader = WafBenchLoader::new();
716        let meta = WafBenchMeta {
717            author: None,
718            description: None,
719            enabled: true,
720            name: None,
721        };
722
723        assert_eq!(
724            loader.detect_category(Path::new("/wafbench/REQUEST-941-XSS/test.yaml"), &meta),
725            SecurityCategory::Xss
726        );
727
728        assert_eq!(
729            loader.detect_category(Path::new("/wafbench/REQUEST-942-SQLI/test.yaml"), &meta),
730            SecurityCategory::SqlInjection
731        );
732    }
733
734    #[test]
735    fn test_looks_like_attack() {
736        let loader = WafBenchLoader::new();
737
738        assert!(loader.looks_like_attack("<script>alert(1)</script>"));
739        assert!(loader.looks_like_attack("' OR '1'='1"));
740        assert!(loader.looks_like_attack("../../../etc/passwd"));
741        assert!(loader.looks_like_attack("; ls -la"));
742        assert!(!loader.looks_like_attack("normal text"));
743        assert!(!loader.looks_like_attack("hello world"));
744    }
745
746    #[test]
747    fn test_extract_payload_value() {
748        let loader = WafBenchLoader::new();
749
750        let uri = "/test?param=%3Cscript%3Ealert(1)%3C/script%3E";
751        let payload = loader.extract_payload_value(uri);
752        assert!(payload.contains("<script>") || payload.contains("script"));
753    }
754
755    #[test]
756    fn test_parse_crs_v33_format() {
757        // CRS v3.3/master uses a nested stage: wrapper
758        let yaml = r#"
759meta:
760  author: "Christian Folini"
761  description: Various SQL injection tests
762  enabled: true
763  name: 942100.yaml
764
765tests:
766  - test_title: 942100-1
767    desc: "Simple SQL Injection"
768    stages:
769      - stage:
770          input:
771            dest_addr: 127.0.0.1
772            headers:
773              Host: localhost
774            method: POST
775            port: 80
776            uri: "/"
777            data: "var=1234 OR 1=1"
778            version: HTTP/1.0
779          output:
780            log_contains: id "942100"
781"#;
782
783        let file: WafBenchFile = serde_yaml::from_str(yaml).unwrap();
784        assert!(file.meta.enabled);
785        assert_eq!(file.tests.len(), 1);
786        assert_eq!(file.tests[0].test_title, "942100-1");
787
788        // Verify we can get the input from nested format
789        let stage = &file.tests[0].stages[0];
790        let input = stage.get_input().expect("Should have input");
791        assert_eq!(input.method, "POST");
792        assert_eq!(input.data.as_deref(), Some("var=1234 OR 1=1"));
793    }
794}