1#![allow(dead_code)]
13pub mod advanced;
19pub mod secure_executor;
20
21pub 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#[derive(Debug, Clone)]
40pub struct SecurityConfig {
41 pub max_execution_time: Duration,
43 pub max_memory_usage: usize,
45 pub max_result_count: usize,
47 pub max_complexity_score: f64,
49 pub enable_injection_detection: bool,
51 pub allowed_functions: HashSet<String>,
53 pub allowed_prefixes: HashSet<String>,
55 pub enable_sandboxing: bool,
57 pub enable_security_logging: bool,
59 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, 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#[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 pub fn new(config: SecurityConfig) -> Result<Self> {
140 let dangerous_patterns = vec![
141 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 Regex::new(r"(?i)\b(?:union|optional|exists|not\s+exists)\s*\{")?,
148 Regex::new(r"\{[^}]*\{[^}]*\{[^}]*\{")?,
149 Regex::new(r"(?i)\b(?:count|sum|avg|min|max)\s*\(\s*\*\s*\)")?,
151 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 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 if self.config.enable_injection_detection {
181 self.check_dangerous_patterns(query, &mut analysis)?;
182 }
183
184 self.analyze_complexity(query, &mut analysis)?;
186
187 self.check_allowed_functions(query, &mut analysis)?;
189
190 self.check_allowed_prefixes(query, &mut analysis)?;
192
193 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 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 fn analyze_complexity(&self, query: &str, analysis: &mut SecurityAnalysisResult) -> Result<()> {
235 let mut complexity = 0.0;
236
237 let triple_pattern_count = query.matches('?').count();
239 complexity += triple_pattern_count as f64 * 1.0;
240
241 let join_count = query.matches("JOIN").count();
243 complexity += join_count as f64 * 2.0;
244
245 let union_count = query.matches("UNION").count();
247 complexity += union_count as f64 * 1.5;
248
249 let optional_count = query.matches("OPTIONAL").count();
251 complexity += optional_count as f64 * 1.2;
252
253 let nesting_depth = self.calculate_nesting_depth(query);
255 complexity += (nesting_depth as f64).powi(2) * 0.5;
256
257 let aggregation_count = query.matches("GROUP BY").count() + query.matches("HAVING").count();
259 complexity += aggregation_count as f64 * 2.0;
260
261 let subquery_count = query.matches("SELECT").count().saturating_sub(1); complexity += subquery_count as f64 * 3.0;
264
265 analysis.complexity_score = complexity;
266
267 analysis.estimated_execution_time = Duration::from_millis((complexity * 100.0) as u64);
269
270 analysis.estimated_memory_usage = (complexity * 1024.0 * 1024.0) as usize; 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 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 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 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 fn check_resource_limits(&self, _analysis: &SecurityAnalysisResult) -> Result<()> {
361 Ok(())
363 }
364
365 pub fn sanitize_query(&self, query: &str) -> Result<String> {
367 let mut sanitized = query.to_string();
368
369 sanitized = self.remove_comments(&sanitized);
371
372 sanitized = self.normalize_whitespace(&sanitized);
374
375 sanitized = self.escape_literals(&sanitized);
377
378 Ok(sanitized)
379 }
380
381 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 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 fn escape_literals(&self, query: &str) -> String {
397 query.to_string()
400 }
401}
402
403#[derive(Debug, Clone)]
405pub struct SecurityAnalysisResult {
406 pub is_safe: bool,
408 pub violations: Vec<SecurityViolation>,
410 pub complexity_score: f64,
412 pub estimated_execution_time: Duration,
414 pub estimated_memory_usage: usize,
416 pub analysis_time: Duration,
418}
419
420#[derive(Debug, Clone)]
422pub struct SecurityViolation {
423 pub violation_type: SecurityViolationType,
425 pub severity: SecuritySeverity,
427 pub message: String,
429 pub pattern_id: Option<usize>,
431 pub position: Option<usize>,
433}
434
435#[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#[derive(Debug, Clone, PartialEq, PartialOrd)]
450pub enum SecuritySeverity {
451 Low,
452 Medium,
453 High,
454 Critical,
455}
456
457#[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 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 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 pub fn check_execution_limits(&mut self) -> Result<()> {
487 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 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 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 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#[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 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#[derive(Debug, Clone)]
576pub struct ExecutionStats {
577 pub execution_time: Duration,
578 pub memory_used: usize,
579 pub results_produced: usize,
580}
581
582#[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 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 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 assert!(monitor.enter_shape("shape1").is_err());
770
771 monitor.exit_shape("shape1");
773 monitor
774 .enter_shape("shape1")
775 .expect("shape entry should succeed");
776 }
777}