Skip to main content

oxirs_shacl/
security.rs

1//! SHACL Security Module
2//!
3//! This module provides comprehensive security features for SHACL validation, particularly
4//! for SPARQL constraints to prevent injection attacks and ensure safe execution.
5//!
6//! Features include:
7//! - Query sanitization and analysis
8//! - Execution sandboxing with resource limits
9//! - Advanced injection detection with ML support
10//! - Policy-based execution control
11
12#![allow(dead_code)]
13//! - Rate limiting and quota management
14//! - Comprehensive audit logging
15//! - Security context management
16//! - Query rewriting for security
17
18pub mod advanced;
19pub mod secure_executor;
20
21// Re-export key types for convenience
22pub use advanced::{
23    ExecutionConstraints, InjectionAnalysisResult, Permission, PolicyAuthorizationResult,
24    SecurityConstraints, SecurityContext, SecurityEvent, SecurityEventType, SecurityMetrics,
25    SecurityPolicy, SecurityPolicyManager,
26};
27pub use secure_executor::{
28    utils, ComprehensiveSecurityMetrics, SecureConstraintResult, SecureExecutorFactory,
29    SecureQueryResult, SecureSparqlExecutor,
30};
31
32use regex::Regex;
33use std::collections::{HashMap, HashSet};
34use std::time::{Duration, Instant};
35
36use crate::{Result, ShaclError};
37
38/// Security configuration for SHACL validation
39#[derive(Debug, Clone)]
40pub struct SecurityConfig {
41    /// Maximum allowed query execution time
42    pub max_execution_time: Duration,
43    /// Maximum allowed memory usage (bytes)
44    pub max_memory_usage: usize,
45    /// Maximum number of results allowed
46    pub max_result_count: usize,
47    /// Maximum query complexity score
48    pub max_complexity_score: f64,
49    /// Enable SPARQL injection detection
50    pub enable_injection_detection: bool,
51    /// Allowed functions in SPARQL queries
52    pub allowed_functions: HashSet<String>,
53    /// Allowed prefixes in SPARQL queries
54    pub allowed_prefixes: HashSet<String>,
55    /// Enable query sandboxing
56    pub enable_sandboxing: bool,
57    /// Enable detailed security logging
58    pub enable_security_logging: bool,
59    /// Maximum recursion depth for nested shapes
60    pub max_recursion_depth: usize,
61}
62
63impl Default for SecurityConfig {
64    fn default() -> Self {
65        let mut allowed_functions = HashSet::new();
66        allowed_functions.insert("STR".to_string());
67        allowed_functions.insert("LANG".to_string());
68        allowed_functions.insert("DATATYPE".to_string());
69        allowed_functions.insert("IRI".to_string());
70        allowed_functions.insert("URI".to_string());
71        allowed_functions.insert("BNODE".to_string());
72        allowed_functions.insert("STRDT".to_string());
73        allowed_functions.insert("STRLANG".to_string());
74        allowed_functions.insert("UUID".to_string());
75        allowed_functions.insert("STRUUID".to_string());
76        allowed_functions.insert("STRLEN".to_string());
77        allowed_functions.insert("SUBSTR".to_string());
78        allowed_functions.insert("UCASE".to_string());
79        allowed_functions.insert("LCASE".to_string());
80        allowed_functions.insert("STRSTARTS".to_string());
81        allowed_functions.insert("STRENDS".to_string());
82        allowed_functions.insert("CONTAINS".to_string());
83        allowed_functions.insert("STRBEFORE".to_string());
84        allowed_functions.insert("STRAFTER".to_string());
85        allowed_functions.insert("REPLACE".to_string());
86        allowed_functions.insert("REGEX".to_string());
87        allowed_functions.insert("ABS".to_string());
88        allowed_functions.insert("ROUND".to_string());
89        allowed_functions.insert("CEIL".to_string());
90        allowed_functions.insert("FLOOR".to_string());
91        allowed_functions.insert("RAND".to_string());
92        allowed_functions.insert("NOW".to_string());
93        allowed_functions.insert("YEAR".to_string());
94        allowed_functions.insert("MONTH".to_string());
95        allowed_functions.insert("DAY".to_string());
96        allowed_functions.insert("HOURS".to_string());
97        allowed_functions.insert("MINUTES".to_string());
98        allowed_functions.insert("SECONDS".to_string());
99        allowed_functions.insert("TIMEZONE".to_string());
100        allowed_functions.insert("TZ".to_string());
101
102        let mut allowed_prefixes = HashSet::new();
103        allowed_prefixes.insert("sh".to_string());
104        allowed_prefixes.insert("rdf".to_string());
105        allowed_prefixes.insert("rdfs".to_string());
106        allowed_prefixes.insert("owl".to_string());
107        allowed_prefixes.insert("xsd".to_string());
108        allowed_prefixes.insert("foaf".to_string());
109        allowed_prefixes.insert("dc".to_string());
110        allowed_prefixes.insert("dcterms".to_string());
111        allowed_prefixes.insert("skos".to_string());
112
113        Self {
114            max_execution_time: Duration::from_secs(30),
115            max_memory_usage: 100 * 1024 * 1024, // 100MB
116            max_result_count: 10000,
117            max_complexity_score: 100.0,
118            enable_injection_detection: true,
119            allowed_functions,
120            allowed_prefixes,
121            enable_sandboxing: true,
122            enable_security_logging: true,
123            max_recursion_depth: 10,
124        }
125    }
126}
127
128/// SPARQL query security analyzer
129#[derive(Debug)]
130pub struct SparqlSecurityAnalyzer {
131    config: SecurityConfig,
132    dangerous_patterns: Vec<Regex>,
133    function_extractor: Regex,
134    prefix_extractor: Regex,
135}
136
137impl SparqlSecurityAnalyzer {
138    /// Create a new security analyzer
139    pub fn new(config: SecurityConfig) -> Result<Self> {
140        let dangerous_patterns = vec![
141            // Potential injection patterns
142            Regex::new(r"(?i)\b(drop|delete|insert|clear|create|load)\s+(?:data|graph|silent)")?,
143            Regex::new(r"(?i)\bservice\s+<[^>]*>")?,
144            Regex::new(r"(?i)\bfrom\s+named\s+<[^>]*>")?,
145            Regex::new(r"(?i)\binto\s+(?:graph\s+)?<[^>]*>")?,
146            // Recursive or complex constructs
147            Regex::new(r"(?i)\b(?:union|optional|exists|not\s+exists)\s*\{")?,
148            Regex::new(r"\{[^}]*\{[^}]*\{[^}]*\{")?,
149            // Potential DoS patterns
150            Regex::new(r"(?i)\b(?:count|sum|avg|min|max)\s*\(\s*\*\s*\)")?,
151            // File system access
152            Regex::new(r"(?i)file://")?,
153        ];
154
155        let function_extractor = Regex::new(r"(?i)\b([A-Z_][A-Z0-9_]*)\s*\(")?;
156        let prefix_extractor = Regex::new(r"(?i)prefix\s+([a-z][a-z0-9]*)\s*:")?;
157
158        Ok(Self {
159            config,
160            dangerous_patterns,
161            function_extractor,
162            prefix_extractor,
163        })
164    }
165
166    /// Analyze a SPARQL query for security issues
167    pub fn analyze_query(&self, query: &str) -> Result<SecurityAnalysisResult> {
168        let start_time = Instant::now();
169
170        let mut analysis = SecurityAnalysisResult {
171            is_safe: true,
172            violations: Vec::new(),
173            complexity_score: 0.0,
174            estimated_execution_time: Duration::from_secs(0),
175            estimated_memory_usage: 0,
176            analysis_time: Duration::from_secs(0),
177        };
178
179        // 1. Check for dangerous patterns
180        if self.config.enable_injection_detection {
181            self.check_dangerous_patterns(query, &mut analysis)?;
182        }
183
184        // 2. Analyze query complexity
185        self.analyze_complexity(query, &mut analysis)?;
186
187        // 3. Check allowed functions
188        self.check_allowed_functions(query, &mut analysis)?;
189
190        // 4. Check allowed prefixes
191        self.check_allowed_prefixes(query, &mut analysis)?;
192
193        // 5. Check resource limits
194        self.check_resource_limits(&analysis)?;
195
196        analysis.analysis_time = start_time.elapsed();
197
198        if self.config.enable_security_logging && !analysis.is_safe {
199            tracing::warn!(
200                "Security violation detected in SPARQL query: {} violations",
201                analysis.violations.len()
202            );
203        }
204
205        Ok(analysis)
206    }
207
208    /// Check for dangerous patterns in the query
209    fn check_dangerous_patterns(
210        &self,
211        query: &str,
212        analysis: &mut SecurityAnalysisResult,
213    ) -> Result<()> {
214        for (i, pattern) in self.dangerous_patterns.iter().enumerate() {
215            if let Some(m) = pattern.find(query) {
216                analysis.is_safe = false;
217                analysis.violations.push(SecurityViolation {
218                    violation_type: SecurityViolationType::DangerousPattern,
219                    severity: SecuritySeverity::High,
220                    message: format!(
221                        "Dangerous pattern detected at position {}: '{}'",
222                        m.start(),
223                        m.as_str()
224                    ),
225                    pattern_id: Some(i),
226                    position: Some(m.start()),
227                });
228            }
229        }
230        Ok(())
231    }
232
233    /// Analyze query complexity
234    fn analyze_complexity(&self, query: &str, analysis: &mut SecurityAnalysisResult) -> Result<()> {
235        let mut complexity = 0.0;
236
237        // Count triple patterns
238        let triple_pattern_count = query.matches('?').count();
239        complexity += triple_pattern_count as f64 * 1.0;
240
241        // Count joins (based on shared variables)
242        let join_count = query.matches("JOIN").count();
243        complexity += join_count as f64 * 2.0;
244
245        // Count unions
246        let union_count = query.matches("UNION").count();
247        complexity += union_count as f64 * 1.5;
248
249        // Count optional blocks
250        let optional_count = query.matches("OPTIONAL").count();
251        complexity += optional_count as f64 * 1.2;
252
253        // Count nested blocks
254        let nesting_depth = self.calculate_nesting_depth(query);
255        complexity += (nesting_depth as f64).powi(2) * 0.5;
256
257        // Count aggregations
258        let aggregation_count = query.matches("GROUP BY").count() + query.matches("HAVING").count();
259        complexity += aggregation_count as f64 * 2.0;
260
261        // Count subqueries
262        let subquery_count = query.matches("SELECT").count().saturating_sub(1); // Subtract main SELECT
263        complexity += subquery_count as f64 * 3.0;
264
265        analysis.complexity_score = complexity;
266
267        // Estimate execution time based on complexity
268        analysis.estimated_execution_time = Duration::from_millis((complexity * 100.0) as u64);
269
270        // Estimate memory usage based on complexity
271        analysis.estimated_memory_usage = (complexity * 1024.0 * 1024.0) as usize; // 1MB per complexity point
272
273        if complexity > self.config.max_complexity_score {
274            analysis.is_safe = false;
275            analysis.violations.push(SecurityViolation {
276                violation_type: SecurityViolationType::ComplexityExceeded,
277                severity: SecuritySeverity::Medium,
278                message: format!(
279                    "Query complexity ({:.1}) exceeds maximum allowed ({})",
280                    complexity, self.config.max_complexity_score
281                ),
282                pattern_id: None,
283                position: None,
284            });
285        }
286
287        Ok(())
288    }
289
290    /// Calculate nesting depth of braces in query
291    fn calculate_nesting_depth(&self, query: &str) -> usize {
292        let mut max_depth = 0_usize;
293        let mut current_depth: i32 = 0;
294
295        for c in query.chars() {
296            match c {
297                '{' => {
298                    current_depth += 1;
299                    max_depth = max_depth.max(current_depth as usize);
300                }
301                '}' => {
302                    current_depth = current_depth.saturating_sub(1);
303                }
304                _ => {}
305            }
306        }
307
308        max_depth
309    }
310
311    /// Check for allowed functions
312    fn check_allowed_functions(
313        &self,
314        query: &str,
315        analysis: &mut SecurityAnalysisResult,
316    ) -> Result<()> {
317        for caps in self.function_extractor.captures_iter(query) {
318            if let Some(function_name) = caps.get(1) {
319                let func = function_name.as_str().to_uppercase();
320                if !self.config.allowed_functions.contains(&func) {
321                    analysis.is_safe = false;
322                    analysis.violations.push(SecurityViolation {
323                        violation_type: SecurityViolationType::DisallowedFunction,
324                        severity: SecuritySeverity::Medium,
325                        message: format!("Disallowed function: {func}"),
326                        pattern_id: None,
327                        position: Some(function_name.start()),
328                    });
329                }
330            }
331        }
332        Ok(())
333    }
334
335    /// Check for allowed prefixes
336    fn check_allowed_prefixes(
337        &self,
338        query: &str,
339        analysis: &mut SecurityAnalysisResult,
340    ) -> Result<()> {
341        for caps in self.prefix_extractor.captures_iter(query) {
342            if let Some(prefix_name) = caps.get(1) {
343                let prefix = prefix_name.as_str().to_lowercase();
344                if !self.config.allowed_prefixes.contains(&prefix) {
345                    analysis.is_safe = false;
346                    analysis.violations.push(SecurityViolation {
347                        violation_type: SecurityViolationType::DisallowedPrefix,
348                        severity: SecuritySeverity::Low,
349                        message: format!("Disallowed prefix: {prefix}"),
350                        pattern_id: None,
351                        position: Some(prefix_name.start()),
352                    });
353                }
354            }
355        }
356        Ok(())
357    }
358
359    /// Check resource limits
360    fn check_resource_limits(&self, _analysis: &SecurityAnalysisResult) -> Result<()> {
361        // These checks are performed during analysis, this method validates final limits
362        Ok(())
363    }
364
365    /// Sanitize a SPARQL query
366    pub fn sanitize_query(&self, query: &str) -> Result<String> {
367        let mut sanitized = query.to_string();
368
369        // Remove potentially dangerous comments
370        sanitized = self.remove_comments(&sanitized);
371
372        // Normalize whitespace
373        sanitized = self.normalize_whitespace(&sanitized);
374
375        // Escape special characters in literals
376        sanitized = self.escape_literals(&sanitized);
377
378        Ok(sanitized)
379    }
380
381    /// Remove comments from query
382    fn remove_comments(&self, query: &str) -> String {
383        let comment_pattern = Regex::new(r"#[^\r\n]*").expect("valid regex pattern");
384        comment_pattern.replace_all(query, "").to_string()
385    }
386
387    /// Normalize whitespace
388    fn normalize_whitespace(&self, query: &str) -> String {
389        let whitespace_pattern = Regex::new(r"\s+").expect("valid regex pattern");
390        whitespace_pattern
391            .replace_all(query.trim(), " ")
392            .to_string()
393    }
394
395    /// Escape literals in query
396    fn escape_literals(&self, query: &str) -> String {
397        // This would implement proper literal escaping
398        // For now, return as-is
399        query.to_string()
400    }
401}
402
403/// Result of security analysis
404#[derive(Debug, Clone)]
405pub struct SecurityAnalysisResult {
406    /// Whether the query is considered safe
407    pub is_safe: bool,
408    /// List of security violations found
409    pub violations: Vec<SecurityViolation>,
410    /// Calculated complexity score
411    pub complexity_score: f64,
412    /// Estimated execution time
413    pub estimated_execution_time: Duration,
414    /// Estimated memory usage in bytes
415    pub estimated_memory_usage: usize,
416    /// Time taken for analysis
417    pub analysis_time: Duration,
418}
419
420/// Security violation details
421#[derive(Debug, Clone)]
422pub struct SecurityViolation {
423    /// Type of security violation
424    pub violation_type: SecurityViolationType,
425    /// Severity of the violation
426    pub severity: SecuritySeverity,
427    /// Human-readable message
428    pub message: String,
429    /// Pattern ID if applicable
430    pub pattern_id: Option<usize>,
431    /// Position in query where violation was found
432    pub position: Option<usize>,
433}
434
435/// Types of security violations
436#[derive(Debug, Clone, PartialEq)]
437pub enum SecurityViolationType {
438    DangerousPattern,
439    ComplexityExceeded,
440    DisallowedFunction,
441    DisallowedPrefix,
442    ExecutionTimeExceeded,
443    MemoryLimitExceeded,
444    ResultCountExceeded,
445    RecursionDepthExceeded,
446}
447
448/// Security violation severity levels
449#[derive(Debug, Clone, PartialEq, PartialOrd)]
450pub enum SecuritySeverity {
451    Low,
452    Medium,
453    High,
454    Critical,
455}
456
457/// Query execution sandbox
458#[derive(Debug)]
459pub struct QueryExecutionSandbox {
460    config: SecurityConfig,
461    start_time: Option<Instant>,
462    memory_monitor: MemoryMonitor,
463    result_counter: usize,
464}
465
466impl QueryExecutionSandbox {
467    /// Create a new execution sandbox
468    pub fn new(config: SecurityConfig) -> Self {
469        Self {
470            config,
471            start_time: None,
472            memory_monitor: MemoryMonitor::new(),
473            result_counter: 0,
474        }
475    }
476
477    /// Start monitoring query execution
478    pub fn start_execution(&mut self) -> Result<()> {
479        self.start_time = Some(Instant::now());
480        self.memory_monitor.start()?;
481        self.result_counter = 0;
482        Ok(())
483    }
484
485    /// Check if execution should continue (called periodically)
486    pub fn check_execution_limits(&mut self) -> Result<()> {
487        // Check execution time
488        if let Some(start_time) = self.start_time {
489            if start_time.elapsed() > self.config.max_execution_time {
490                return Err(ShaclError::SecurityViolation(
491                    "Query execution time exceeded".to_string(),
492                ));
493            }
494        }
495
496        // Check memory usage
497        if self.memory_monitor.current_usage() > self.config.max_memory_usage {
498            return Err(ShaclError::SecurityViolation(
499                "Memory usage limit exceeded".to_string(),
500            ));
501        }
502
503        Ok(())
504    }
505
506    /// Record a result and check count limits
507    pub fn record_result(&mut self) -> Result<()> {
508        self.result_counter += 1;
509        if self.result_counter > self.config.max_result_count {
510            return Err(ShaclError::SecurityViolation(
511                "Result count limit exceeded".to_string(),
512            ));
513        }
514        Ok(())
515    }
516
517    /// Stop monitoring and clean up
518    pub fn stop_execution(&mut self) -> Result<ExecutionStats> {
519        let execution_time = self.start_time.map(|t| t.elapsed()).unwrap_or_default();
520        let memory_used = self.memory_monitor.peak_usage();
521        let results_produced = self.result_counter;
522
523        self.memory_monitor.stop()?;
524        self.start_time = None;
525
526        Ok(ExecutionStats {
527            execution_time,
528            memory_used,
529            results_produced,
530        })
531    }
532}
533
534/// Memory usage monitor
535#[derive(Debug)]
536struct MemoryMonitor {
537    peak_usage: usize,
538    current_usage: usize,
539    monitoring: bool,
540}
541
542impl MemoryMonitor {
543    fn new() -> Self {
544        Self {
545            peak_usage: 0,
546            current_usage: 0,
547            monitoring: false,
548        }
549    }
550
551    fn start(&mut self) -> Result<()> {
552        self.monitoring = true;
553        self.peak_usage = 0;
554        self.current_usage = 0;
555        Ok(())
556    }
557
558    fn current_usage(&self) -> usize {
559        // In a real implementation, this would track actual memory usage
560        // For now, return estimated usage
561        self.current_usage
562    }
563
564    fn peak_usage(&self) -> usize {
565        self.peak_usage
566    }
567
568    fn stop(&mut self) -> Result<()> {
569        self.monitoring = false;
570        Ok(())
571    }
572}
573
574/// Execution statistics
575#[derive(Debug, Clone)]
576pub struct ExecutionStats {
577    pub execution_time: Duration,
578    pub memory_used: usize,
579    pub results_produced: usize,
580}
581
582/// Recursive shape validation monitor
583#[derive(Debug)]
584pub struct RecursionMonitor {
585    visited_shapes: HashMap<String, usize>,
586    max_depth: usize,
587}
588
589impl RecursionMonitor {
590    pub fn new(max_depth: usize) -> Self {
591        Self {
592            visited_shapes: HashMap::new(),
593            max_depth,
594        }
595    }
596
597    pub fn enter_shape(&mut self, shape_id: &str) -> Result<()> {
598        let depth = self.visited_shapes.entry(shape_id.to_string()).or_insert(0);
599
600        if *depth >= self.max_depth {
601            return Err(ShaclError::SecurityViolation(format!(
602                "Maximum recursion depth ({}) exceeded for shape {}",
603                self.max_depth, shape_id
604            )));
605        }
606
607        *depth += 1;
608        Ok(())
609    }
610
611    pub fn exit_shape(&mut self, shape_id: &str) {
612        if let Some(depth) = self.visited_shapes.get_mut(shape_id) {
613            *depth = depth.saturating_sub(1);
614        }
615    }
616
617    pub fn current_depth(&self, shape_id: &str) -> usize {
618        self.visited_shapes.get(shape_id).copied().unwrap_or(0)
619    }
620}
621
622#[cfg(test)]
623mod tests {
624    use super::*;
625
626    #[test]
627    fn test_security_analyzer_creation() {
628        let config = SecurityConfig::default();
629        let analyzer = SparqlSecurityAnalyzer::new(config).expect("construction should succeed");
630        assert!(!analyzer.dangerous_patterns.is_empty());
631    }
632
633    #[test]
634    fn test_safe_query_analysis() {
635        let config = SecurityConfig::default();
636        let analyzer = SparqlSecurityAnalyzer::new(config).expect("construction should succeed");
637
638        let safe_query = "SELECT ?s ?p ?o WHERE { ?s ?p ?o }";
639        let result = analyzer
640            .analyze_query(safe_query)
641            .expect("analysis should succeed");
642
643        assert!(result.is_safe);
644        assert!(result.violations.is_empty());
645    }
646
647    #[test]
648    fn test_dangerous_pattern_detection() {
649        let config = SecurityConfig::default();
650        let analyzer = SparqlSecurityAnalyzer::new(config).expect("construction should succeed");
651
652        let dangerous_query = "DROP GRAPH <http://example.org/graph>";
653        let result = analyzer
654            .analyze_query(dangerous_query)
655            .expect("analysis should succeed");
656
657        assert!(!result.is_safe);
658        assert!(!result.violations.is_empty());
659        assert_eq!(
660            result.violations[0].violation_type,
661            SecurityViolationType::DangerousPattern
662        );
663    }
664
665    #[test]
666    fn test_complexity_analysis() {
667        let config = SecurityConfig {
668            max_complexity_score: 5.0,
669            ..SecurityConfig::default()
670        };
671        let analyzer = SparqlSecurityAnalyzer::new(config).expect("construction should succeed");
672
673        let complex_query = r"
674            SELECT ?s ?p ?o ?x ?y ?z WHERE {
675                ?s ?p ?o .
676                ?x ?y ?z .
677                OPTIONAL { ?s ?p2 ?o2 }
678                OPTIONAL { ?x ?y2 ?z2 }
679                { ?a ?b ?c } UNION { ?d ?e ?f }
680            }
681        ";
682        let result = analyzer
683            .analyze_query(complex_query)
684            .expect("analysis should succeed");
685
686        assert!(!result.is_safe);
687        assert!(result
688            .violations
689            .iter()
690            .any(|v| v.violation_type == SecurityViolationType::ComplexityExceeded));
691    }
692
693    #[test]
694    fn test_disallowed_function_detection() {
695        let mut config = SecurityConfig::default();
696        config.allowed_functions.clear();
697        config.allowed_functions.insert("STR".to_string());
698
699        let analyzer = SparqlSecurityAnalyzer::new(config).expect("construction should succeed");
700
701        let query_with_disallowed_function = "SELECT ?s WHERE { ?s ?p ?o . FILTER(RAND() > 0.5) }";
702        let result = analyzer
703            .analyze_query(query_with_disallowed_function)
704            .expect("analysis should succeed");
705
706        assert!(!result.is_safe);
707        assert!(result
708            .violations
709            .iter()
710            .any(|v| v.violation_type == SecurityViolationType::DisallowedFunction));
711    }
712
713    #[test]
714    fn test_query_sanitization() {
715        let config = SecurityConfig::default();
716        let analyzer = SparqlSecurityAnalyzer::new(config).expect("construction should succeed");
717
718        let query_with_comments = r"
719            SELECT ?s ?p ?o WHERE {
720                ?s ?p ?o .  # This is a comment
721                # Another comment
722            }
723        ";
724
725        let sanitized = analyzer
726            .sanitize_query(query_with_comments)
727            .expect("analysis should succeed");
728        assert!(!sanitized.contains('#'));
729    }
730
731    #[test]
732    fn test_execution_sandbox() {
733        let config = SecurityConfig::default();
734        let mut sandbox = QueryExecutionSandbox::new(config);
735
736        sandbox.start_execution().expect("start should succeed");
737
738        // Should be able to record results within limit
739        for _ in 0..10 {
740            sandbox.record_result().expect("recording should succeed");
741        }
742
743        sandbox
744            .check_execution_limits()
745            .expect("check should succeed");
746
747        let stats = sandbox
748            .stop_execution()
749            .expect("sandbox operation should succeed");
750        assert_eq!(stats.results_produced, 10);
751    }
752
753    #[test]
754    fn test_recursion_monitor() {
755        let mut monitor = RecursionMonitor::new(3);
756
757        // Should allow normal recursion
758        monitor
759            .enter_shape("shape1")
760            .expect("shape entry should succeed");
761        monitor
762            .enter_shape("shape1")
763            .expect("shape entry should succeed");
764        monitor
765            .enter_shape("shape1")
766            .expect("shape entry should succeed");
767
768        // Should fail on exceeding depth
769        assert!(monitor.enter_shape("shape1").is_err());
770
771        // Should recover after exiting
772        monitor.exit_shape("shape1");
773        monitor
774            .enter_shape("shape1")
775            .expect("shape entry should succeed");
776    }
777}