Skip to main content

kindly_guard_server/scanner/
sync_wrapper.rs

1// Copyright 2025 Kindly Software Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14//! Synchronous wrapper for the SecurityScanner
15//!
16//! This module provides a synchronous interface to the SecurityScanner
17//! for use in tests and other non-async contexts.
18
19use super::{ScanError, SecurityScanner, Threat};
20use crate::config::ScannerConfig;
21use std::sync::Arc;
22
23/// A synchronous wrapper around SecurityScanner
24///
25/// This wrapper uses a dedicated tokio runtime to handle async operations
26/// internally, providing a synchronous API for testing and other use cases.
27pub struct SyncSecurityScanner {
28    scanner: Arc<SecurityScanner>,
29    #[allow(dead_code)] // Runtime kept for potential async operations
30    runtime: tokio::runtime::Runtime,
31}
32
33impl SyncSecurityScanner {
34    /// Create a new synchronous scanner
35    pub fn new(config: ScannerConfig) -> Result<Self, ScanError> {
36        let runtime = tokio::runtime::Runtime::new()
37            .map_err(|e| ScanError::InvalidInput(format!("Failed to create runtime: {}", e)))?;
38
39        let scanner = Arc::new(SecurityScanner::new(config)?);
40
41        Ok(Self { scanner, runtime })
42    }
43
44    /// Scan text synchronously
45    pub fn scan_text(&self, text: &str) -> Result<Vec<Threat>, ScanError> {
46        // For synchronous scanning, we'll skip the XSS scanner which requires async
47        let mut threats = Vec::new();
48
49        if self.scanner.config.unicode_detection {
50            threats.extend(self.scanner.unicode_scanner.scan_text(text)?);
51        }
52
53        if self.scanner.config.injection_detection {
54            threats.extend(self.scanner.injection_scanner.scan_text(text)?);
55        }
56
57        // Skip XSS scanner in sync mode as it requires async runtime
58        // Skip plugin scanners in sync mode as they may require async
59
60        Ok(threats)
61    }
62
63    /// Scan JSON synchronously
64    pub fn scan_json(&self, value: &serde_json::Value) -> Result<Vec<Threat>, ScanError> {
65        // Convert JSON to string and scan
66        let json_str = serde_json::to_string(value)
67            .map_err(|e| ScanError::InvalidInput(format!("Invalid JSON: {}", e)))?;
68
69        self.scan_text(&json_str)
70    }
71}
72
73/// Create a scanner suitable for synchronous testing
74///
75/// This creates a scanner with XSS detection disabled to avoid async requirements
76pub fn create_sync_scanner(config: ScannerConfig) -> Result<SecurityScanner, ScanError> {
77    let mut sync_config = config;
78    sync_config.xss_detection = Some(false); // Disable XSS to avoid async
79    SecurityScanner::new(sync_config)
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85    use crate::scanner::ThreatType;
86
87    #[test]
88    fn test_sync_scanner_basic() {
89        let config = ScannerConfig {
90            unicode_detection: true,
91            injection_detection: true,
92            path_traversal_detection: true,
93            xss_detection: Some(false),
94            crypto_detection: true,
95            enhanced_mode: Some(false),
96            custom_patterns: None,
97            max_scan_depth: 10,
98            enable_event_buffer: false,
99            max_content_size: 5 * 1024 * 1024, // 5MB
100            max_input_size: None,
101            allow_text_control_chars: false,
102        };
103        let scanner = SyncSecurityScanner::new(config).unwrap();
104
105        let threats = scanner
106            .scan_text("SELECT * FROM users WHERE id = '1' OR '1'='1'")
107            .unwrap();
108        assert!(!threats.is_empty());
109        assert!(threats
110            .iter()
111            .any(|t| matches!(t.threat_type, ThreatType::SqlInjection)));
112    }
113
114    #[test]
115    fn test_sync_scanner_unicode() {
116        let config = ScannerConfig {
117            unicode_detection: true,
118            injection_detection: true,
119            path_traversal_detection: true,
120            xss_detection: Some(false),
121            crypto_detection: true,
122            enhanced_mode: Some(false),
123            custom_patterns: None,
124            max_scan_depth: 10,
125            enable_event_buffer: false,
126            max_content_size: 5 * 1024 * 1024, // 5MB
127            max_input_size: None,
128            allow_text_control_chars: false,
129        };
130        let scanner = SyncSecurityScanner::new(config).unwrap();
131
132        let threats = scanner.scan_text("Hello\u{202E}World").unwrap();
133        assert!(!threats.is_empty());
134        assert!(threats
135            .iter()
136            .any(|t| matches!(t.threat_type, ThreatType::UnicodeBiDi)));
137    }
138
139    #[test]
140    fn test_create_sync_scanner() {
141        let config = ScannerConfig {
142            unicode_detection: true,
143            injection_detection: true,
144            path_traversal_detection: true,
145            xss_detection: Some(false),
146            crypto_detection: true,
147            enhanced_mode: Some(false),
148            custom_patterns: None,
149            max_scan_depth: 10,
150            enable_event_buffer: false,
151            max_content_size: 5 * 1024 * 1024, // 5MB
152            max_input_size: None,
153            allow_text_control_chars: false,
154        };
155        let scanner = create_sync_scanner(config).unwrap();
156
157        // This should work without async runtime
158        let threats = scanner.scan_text("'; DROP TABLE users; --").unwrap();
159        assert!(!threats.is_empty());
160    }
161}