1use std::sync::Arc;
5use std::time::Instant;
6
7use anyhow::Result;
9use globset;
10
11use crate::binary::format::{FileFormat, FormatDetector};
13use crate::config::settings::{RiskLevel, SecurityConfig};
14use crate::filesystem::{
15 validation::{PathValidator, SecurityError},
16 FileOperation,
17};
18use crate::mcp::OperationType;
19use crate::security::approval::ApprovalDecision;
20use crate::security::audit::{AuditLogger, CorrelationId};
21use crate::security::permissions::{
22 PathPermissionRule, PathPermissionValidator, PermissionEvaluation, PermissionLevel,
23};
24use crate::security::policy::PolicyEngine;
25
26#[derive(Debug)]
28pub struct SecurityManager {
29 path_validator: PathValidator,
30 policy_engine: PolicyEngine,
31 permission_validator: PathPermissionValidator,
32 audit_logger: AuditLogger,
33 format_detector: FormatDetector,
34 config: Arc<SecurityConfig>,
35}
36
37impl SecurityManager {
38 pub fn new(config: SecurityConfig) -> Result<Self> {
40 let path_validator = PathValidator::new(
41 config.filesystem.allowed_paths.clone(),
42 config.filesystem.denied_paths.clone(),
43 );
44
45 let policy_engine = PolicyEngine::new(config.policies.clone())?;
47
48 let mut permission_validator = PathPermissionValidator::new(true); for (name, policy) in &config.policies {
55 permission_validator.add_policy(name.clone(), policy.clone());
56
57 for pattern in &policy.patterns {
59 let permission_level = if policy.operations.contains(&"delete".to_string()) {
61 PermissionLevel::Full
62 } else if policy.operations.contains(&"write".to_string()) {
63 PermissionLevel::ReadWrite
64 } else if policy.operations.contains(&"read".to_string()) {
65 PermissionLevel::ReadOnly
66 } else {
67 PermissionLevel::None
68 };
69
70 let rule = PathPermissionRule::new(
71 pattern.clone(),
72 permission_level,
73 policy.operations.iter().map(|s| s.as_str()).collect(),
74 100, format!("Policy '{name}' rule for pattern: {pattern}"),
76 )?;
77
78 permission_validator.add_rule(rule);
79 }
80 }
81
82 Ok(Self {
83 path_validator,
84 policy_engine,
85 permission_validator,
86 audit_logger: AuditLogger::new(),
87 format_detector: FormatDetector::new(),
88 config: Arc::new(config),
89 })
90 }
91
92 pub async fn validate_operation_permission(
97 &self,
98 operation: &FileOperation,
99 ) -> Result<ApprovalDecision> {
100 let correlation_id = CorrelationId::new();
101 let start_time = Instant::now();
102
103 self.audit_logger
105 .log_operation_requested(correlation_id, operation);
106
107 if let Err(err) = self
109 .validate_binary_file_restriction(&operation.path, correlation_id)
110 .await
111 {
112 let execution_time_ms = start_time.elapsed().as_millis() as u64;
113 self.audit_logger.log_operation_failed(
114 correlation_id,
115 operation,
116 &err.to_string(),
117 execution_time_ms,
118 );
119 return Err(err);
120 }
121
122 match self.path_validator.validate_path(&operation.path) {
124 Ok(_validated_path) => {}
125 Err(security_error) => {
126 let execution_time_ms = start_time.elapsed().as_millis() as u64;
127 let error_msg = match security_error {
128 SecurityError::AccessDenied => "Access denied",
129 SecurityError::InvalidInput => "Invalid input detected",
130 SecurityError::PolicyViolation => "Security policy violation",
131 };
132 self.audit_logger.log_operation_failed(
133 correlation_id,
134 operation,
135 error_msg,
136 execution_time_ms,
137 );
138 return Err(anyhow::anyhow!("{}", error_msg));
139 }
140 }
141
142 let operations = std::iter::once(operation.operation_type).collect();
144 let permission_result = self.permission_validator.evaluate_permissions(
145 &operation.path,
146 &operations,
147 Some(&format!(
148 "operation_validation_{:?}",
149 operation.operation_type
150 )),
151 );
152
153 if !permission_result.allowed {
154 let execution_time_ms = start_time.elapsed().as_millis() as u64;
155 let reason = format!(
156 "Operation {:?} denied by permission system: {}",
157 operation.operation_type, permission_result.decision_reason
158 );
159 self.audit_logger.log_operation_failed(
160 correlation_id,
161 operation,
162 &reason,
163 execution_time_ms,
164 );
165 return Err(anyhow::anyhow!("{}", reason));
166 }
167
168 let operation_allowed = match operation.operation_type {
170 OperationType::Read => {
171 self.config.operations.read_allowed
173 }
174 OperationType::Write => {
175 if self.config.operations.write_requires_policy {
177 self.validate_operation_against_policies(operation, correlation_id)
179 .await?
180 } else {
181 true
183 }
184 }
185 OperationType::Delete => {
186 if self.config.operations.delete_requires_explicit_allow {
188 self.validate_delete_permission(operation, correlation_id)
190 .await?
191 } else {
192 true
194 }
195 }
196 OperationType::CreateDir => {
197 self.config.operations.create_dir_allowed
199 }
200 OperationType::List | OperationType::Move | OperationType::Copy => {
201 true
203 }
204 };
205
206 if !operation_allowed {
207 let execution_time_ms = start_time.elapsed().as_millis() as u64;
208 let reason = format!(
209 "Operation {:?} denied by operation configuration",
210 operation.operation_type
211 );
212 self.audit_logger.log_operation_failed(
213 correlation_id,
214 operation,
215 &reason,
216 execution_time_ms,
217 );
218 return Err(anyhow::anyhow!("{}", reason));
219 }
220
221 let policy_start = Instant::now();
223 let policy_decision = self.policy_engine.evaluate_operation(operation);
224 let policy_time_ms = policy_start.elapsed().as_millis() as u64;
225
226 self.audit_logger.log_policy_evaluated(
228 correlation_id,
229 &policy_decision,
230 None,
231 policy_time_ms,
232 );
233
234 let execution_time_ms = start_time.elapsed().as_millis() as u64;
235
236 if policy_decision.is_allowed() {
237 self.audit_logger.log_operation_completed(
239 correlation_id,
240 operation,
241 execution_time_ms,
242 None,
243 );
244 Ok(ApprovalDecision::Approved)
245 } else {
246 let reason = format!(
248 "Operation {:?} denied by policy engine: {}",
249 operation.operation_type,
250 policy_decision.reason()
251 );
252 self.audit_logger.log_operation_failed(
253 correlation_id,
254 operation,
255 &reason,
256 execution_time_ms,
257 );
258 Err(anyhow::anyhow!("{}", reason))
259 }
260 }
261
262 async fn validate_binary_file_restriction(
265 &self,
266 path: &std::path::Path,
267 correlation_id: CorrelationId,
268 ) -> Result<()> {
269 let format_from_extension = self.format_detector.detect_from_extension(path);
271
272 if self.is_binary_format(&format_from_extension) {
274 let error_msg = format!(
275 "Binary file access denied for security reasons: {} (detected format: {:?})",
276 path.display(),
277 format_from_extension
278 );
279
280 self.audit_logger.log_security_violation(
282 correlation_id,
283 "binary_file_denied",
284 path,
285 &error_msg,
286 RiskLevel::High,
287 );
288
289 return Err(anyhow::anyhow!("{}", error_msg));
290 }
291
292 if path.exists() && path.is_file() {
295 match std::fs::read(path) {
296 Ok(bytes) if !bytes.is_empty() => {
297 let sample_size = std::cmp::min(bytes.len(), 512);
299 let format_from_content = self
300 .format_detector
301 .detect_from_bytes(&bytes[..sample_size]);
302
303 if self.is_binary_format(&format_from_content) {
304 let error_msg = format!(
305 "Binary file access denied for security reasons: {} (content analysis: {:?})",
306 path.display(),
307 format_from_content
308 );
309
310 self.audit_logger.log_security_violation(
312 correlation_id,
313 "binary_content_denied",
314 path,
315 &error_msg,
316 RiskLevel::High,
317 );
318
319 return Err(anyhow::anyhow!("{}", error_msg));
320 }
321 }
322 Err(_) => {
323 }
326 Ok(_) => {
327 }
329 }
330 }
331
332 Ok(())
334 }
335
336 fn is_binary_format(&self, format: &FileFormat) -> bool {
338 match format {
339 FileFormat::Jpeg
341 | FileFormat::Png
342 | FileFormat::Gif
343 | FileFormat::WebP
344 | FileFormat::Tiff
345 | FileFormat::Bmp => true,
346
347 FileFormat::Pdf => true,
349
350 FileFormat::Text => false,
352
353 FileFormat::Unknown => false,
356 }
357 }
358
359 async fn validate_operation_against_policies(
361 &self,
362 operation: &FileOperation,
363 correlation_id: CorrelationId,
364 ) -> Result<bool> {
365 for policy in self.config.policies.values() {
367 for pattern in &policy.patterns {
369 if let Ok(glob) = globset::Glob::new(pattern) {
370 if glob.compile_matcher().is_match(&operation.path) {
371 let operation_name = operation.operation_type.as_str();
373
374 if policy.operations.contains(&operation_name.to_string()) {
375 return Ok(true);
378 }
379 }
380 }
381 }
382 }
383
384 self.audit_logger.log_security_violation(
386 correlation_id,
387 "policy_denied",
388 &operation.path,
389 &format!(
390 "Operation {:?} denied - no matching policy found",
391 operation.operation_type
392 ),
393 RiskLevel::High,
394 );
395
396 Ok(false)
397 }
398
399 async fn validate_delete_permission(
401 &self,
402 operation: &FileOperation,
403 correlation_id: CorrelationId,
404 ) -> Result<bool> {
405 for policy in self.config.policies.values() {
407 for pattern in &policy.patterns {
409 if let Ok(glob) = globset::Glob::new(pattern) {
410 if glob.compile_matcher().is_match(&operation.path) {
411 if policy.operations.contains(&"delete".to_string()) {
413 return Ok(true);
415 }
416 }
417 }
418 }
419 }
420
421 self.audit_logger.log_security_violation(
423 correlation_id,
424 "delete_denied",
425 &operation.path,
426 "Delete operation denied - no explicit delete permission found",
427 RiskLevel::High,
428 );
429
430 Ok(false)
431 }
432
433 pub async fn validate_read_access(&self, operation: &FileOperation) -> Result<()> {
435 let correlation_id = CorrelationId::new();
436 let start_time = Instant::now();
437
438 self.audit_logger
440 .log_operation_requested(correlation_id, operation);
441
442 match self.path_validator.validate_path(&operation.path) {
444 Ok(_validated_path) => {}
445 Err(security_error) => {
446 let execution_time_ms = start_time.elapsed().as_millis() as u64;
447 let error_msg = match security_error {
448 SecurityError::AccessDenied => "Access denied",
449 SecurityError::InvalidInput => "Invalid input detected",
450 SecurityError::PolicyViolation => "Security policy violation",
451 };
452 self.audit_logger.log_operation_failed(
453 correlation_id,
454 operation,
455 error_msg,
456 execution_time_ms,
457 );
458 return Err(anyhow::anyhow!("{}", error_msg));
459 }
460 }
461
462 let operations = std::iter::once(operation.operation_type).collect();
464 let permission_result = self.permission_validator.evaluate_permissions(
465 &operation.path,
466 &operations,
467 Some("read_access_validation"),
468 );
469
470 if !permission_result.allowed {
471 let execution_time_ms = start_time.elapsed().as_millis() as u64;
472 self.audit_logger.log_operation_failed(
473 correlation_id,
474 operation,
475 &format!("Permission denied: {}", permission_result.decision_reason),
476 execution_time_ms,
477 );
478 return Err(anyhow::anyhow!(
479 "Permission denied: {}",
480 permission_result.decision_reason
481 ));
482 }
483
484 let policy_start = Instant::now();
486 let policy_decision = self.policy_engine.evaluate_operation(operation);
487 let policy_time_ms = policy_start.elapsed().as_millis() as u64;
488
489 self.audit_logger.log_policy_evaluated(
491 correlation_id,
492 &policy_decision,
493 None, policy_time_ms,
495 );
496
497 let execution_time_ms = start_time.elapsed().as_millis() as u64;
498
499 if policy_decision.is_allowed() {
500 self.audit_logger.log_operation_completed(
502 correlation_id,
503 operation,
504 execution_time_ms,
505 None, );
507 Ok(())
508 } else {
509 self.audit_logger.log_operation_failed(
511 correlation_id,
512 operation,
513 &format!("Policy denied read access: {}", policy_decision.reason()),
514 execution_time_ms,
515 );
516 Err(anyhow::anyhow!(
517 "Policy denied read access: {}",
518 policy_decision.reason()
519 ))
520 }
521 }
522
523 pub async fn validate_write_access(
525 &self,
526 operation: &FileOperation,
527 ) -> Result<ApprovalDecision> {
528 let correlation_id = CorrelationId::new();
529 let start_time = Instant::now();
530
531 self.audit_logger
533 .log_operation_requested(correlation_id, operation);
534
535 match self.path_validator.validate_path(&operation.path) {
537 Ok(_validated_path) => {}
538 Err(security_error) => {
539 let execution_time_ms = start_time.elapsed().as_millis() as u64;
540 let error_msg = match security_error {
541 SecurityError::AccessDenied => "Access denied",
542 SecurityError::InvalidInput => "Invalid input detected",
543 SecurityError::PolicyViolation => "Security policy violation",
544 };
545 self.audit_logger.log_operation_failed(
546 correlation_id,
547 operation,
548 error_msg,
549 execution_time_ms,
550 );
551 return Err(anyhow::anyhow!("{}", error_msg));
552 }
553 }
554
555 let operations = std::iter::once(operation.operation_type).collect();
557 let permission_result = self.permission_validator.evaluate_permissions(
558 &operation.path,
559 &operations,
560 Some("write_access_validation"),
561 );
562
563 if !permission_result.allowed {
564 let execution_time_ms = start_time.elapsed().as_millis() as u64;
565 self.audit_logger.log_operation_failed(
566 correlation_id,
567 operation,
568 &format!("Permission denied: {}", permission_result.decision_reason),
569 execution_time_ms,
570 );
571 return Err(anyhow::anyhow!(
572 "Permission denied: {}",
573 permission_result.decision_reason
574 ));
575 }
576
577 let policy_start = Instant::now();
579 let policy_decision = self.policy_engine.evaluate_operation(operation);
580 let policy_time_ms = policy_start.elapsed().as_millis() as u64;
581
582 self.audit_logger.log_policy_evaluated(
584 correlation_id,
585 &policy_decision,
586 None, policy_time_ms,
588 );
589
590 let execution_time_ms = start_time.elapsed().as_millis() as u64;
591
592 if self.config.operations.write_requires_policy
593 && self.requires_approval(operation.operation_type)
594 {
595 if policy_decision.is_allowed() {
596 let decision = ApprovalDecision::Approved;
598
599 self.audit_logger.log_operation_completed(
600 correlation_id,
601 operation,
602 execution_time_ms,
603 None, );
605
606 Ok(decision)
607 } else {
608 self.audit_logger.log_operation_failed(
610 correlation_id,
611 operation,
612 &format!("Policy denied write access: {}", policy_decision.reason()),
613 execution_time_ms,
614 );
615 Ok(ApprovalDecision::Denied)
616 }
617 } else {
618 if policy_decision.is_allowed() {
620 self.audit_logger.log_operation_completed(
621 correlation_id,
622 operation,
623 execution_time_ms,
624 None, );
626 Ok(ApprovalDecision::Approved)
627 } else {
628 self.audit_logger.log_operation_failed(
629 correlation_id,
630 operation,
631 &format!("Policy denied write access: {}", policy_decision.reason()),
632 execution_time_ms,
633 );
634 Ok(ApprovalDecision::Denied)
635 }
636 }
637 }
638
639 pub fn add_permission_rule(&mut self, rule: PathPermissionRule) {
641 self.permission_validator.add_rule(rule);
642 }
643
644 pub fn evaluate_path_permissions(
646 &self,
647 path: &std::path::Path,
648 operations: &std::collections::HashSet<OperationType>,
649 context: Option<&str>,
650 ) -> PermissionEvaluation {
651 self.permission_validator
652 .evaluate_permissions(path, operations, context)
653 }
654
655 pub fn get_permission_coverage(&self) -> std::collections::HashMap<String, usize> {
657 self.permission_validator.get_coverage_stats()
658 }
659
660 fn requires_approval(&self, operation_type: OperationType) -> bool {
662 match operation_type {
663 OperationType::Read | OperationType::List => false,
664 OperationType::Write
665 | OperationType::CreateDir
666 | OperationType::Delete
667 | OperationType::Move
668 | OperationType::Copy => true,
669 }
670 }
671}
672
673#[cfg(test)]
674#[allow(clippy::unwrap_used, clippy::expect_used)]
675mod tests {
676 use super::*;
677 use crate::config::SecurityConfig;
678 use std::path::PathBuf;
679
680 fn create_test_config() -> SecurityConfig {
681 use crate::config::settings::{
682 FilesystemConfig, OperationConfig, RiskLevel, SecurityPolicy,
683 };
684 use std::collections::HashMap;
685
686 let mut policies = HashMap::new();
687
688 policies.insert(
690 "universal_test_policy".to_string(),
691 SecurityPolicy {
692 patterns: vec!["**/*".to_string()], operations: vec![
694 "read".to_string(),
695 "write".to_string(),
696 "list".to_string(),
697 "create_dir".to_string(),
698 "move".to_string(),
699 "copy".to_string(),
700 "delete".to_string(),
701 ],
702 risk_level: RiskLevel::Low,
703 description: Some("Universal test policy allowing all operations".to_string()),
704 },
705 );
706
707 SecurityConfig {
708 filesystem: FilesystemConfig {
709 allowed_paths: vec!["**/*".to_string()], denied_paths: vec!["**/.git/**".to_string()],
711 },
712 operations: OperationConfig {
713 read_allowed: true,
714 write_requires_policy: false, delete_requires_explicit_allow: false, create_dir_allowed: true,
717 },
718 policies,
719 }
720 }
721
722 #[test]
723 fn test_security_manager_creation() {
724 let config = create_test_config();
725 let manager = SecurityManager::new(config).unwrap();
726
727 assert!(manager.config.operations.read_allowed);
729 assert!(!manager.config.operations.write_requires_policy); assert!(manager.config.operations.create_dir_allowed);
731 }
732
733 #[tokio::test]
734 async fn test_validate_read_access_success() {
735 let config = create_test_config();
736 let manager = SecurityManager::new(config).unwrap();
737
738 let operation = FileOperation::new(OperationType::Read, PathBuf::from("src/main.rs"));
739
740 let result = manager.validate_read_access(&operation).await;
741 assert!(result.is_ok());
742 }
743
744 #[tokio::test]
745 async fn test_validate_read_access_denied_path() {
746 let config = create_test_config();
747 let manager = SecurityManager::new(config).unwrap();
748
749 let operation = FileOperation::new(OperationType::Read, PathBuf::from(".git/config"));
750
751 let result = manager.validate_read_access(&operation).await;
752 assert!(result.is_err());
753 }
754
755 #[tokio::test]
756 async fn test_validate_write_access_with_approval() {
757 let config = create_test_config();
758 let manager = SecurityManager::new(config).unwrap();
759
760 let operation = FileOperation::new(OperationType::Write, PathBuf::from("src/new_file.rs"));
761
762 let result = manager.validate_write_access(&operation).await;
763 assert!(result.is_ok());
764 assert_eq!(result.unwrap(), ApprovalDecision::Approved);
766 }
767
768 #[test]
769 fn test_requires_approval() {
770 let config = create_test_config();
771 let manager = SecurityManager::new(config).unwrap();
772
773 assert!(!manager.requires_approval(OperationType::Read));
774 assert!(!manager.requires_approval(OperationType::List));
775 assert!(manager.requires_approval(OperationType::Write));
776 assert!(manager.requires_approval(OperationType::Delete));
777 assert!(manager.requires_approval(OperationType::CreateDir));
778 }
779
780 #[tokio::test]
783 async fn test_validate_operation_permission_read_success() {
784 let config = create_test_config();
785 let manager = SecurityManager::new(config).unwrap();
786
787 let operation = FileOperation::new(OperationType::Read, PathBuf::from("src/main.rs"));
788
789 let result = manager.validate_operation_permission(&operation).await;
790 assert!(result.is_ok());
791 assert_eq!(result.unwrap(), ApprovalDecision::Approved);
792 }
793
794 #[tokio::test]
795 async fn test_validate_operation_permission_write_success() {
796 let config = create_test_config();
797 let manager = SecurityManager::new(config).unwrap();
798
799 let operation = FileOperation::new(OperationType::Write, PathBuf::from("src/new_file.rs"));
800
801 let result = manager.validate_operation_permission(&operation).await;
802 assert!(result.is_ok());
803 assert_eq!(result.unwrap(), ApprovalDecision::Approved);
804 }
805
806 #[tokio::test]
807 async fn test_validate_operation_permission_delete_success() {
808 use crate::config::settings::{RiskLevel, SecurityPolicy};
809
810 let mut config = create_test_config();
811
812 let delete_policy = SecurityPolicy {
814 patterns: vec!["src/**".to_string()],
815 operations: vec![
816 "read".to_string(),
817 "write".to_string(),
818 "delete".to_string(),
819 ],
820 risk_level: RiskLevel::Low,
821 description: None,
822 };
823 config
824 .policies
825 .insert("delete_policy".to_string(), delete_policy);
826
827 let manager = SecurityManager::new(config).unwrap();
828
829 let operation =
831 FileOperation::new(OperationType::Delete, PathBuf::from("src/temp_file.txt"));
832
833 let result = manager.validate_operation_permission(&operation).await;
834 assert!(result.is_ok());
835 assert_eq!(result.unwrap(), ApprovalDecision::Approved);
836 }
837
838 #[tokio::test]
839 async fn test_validate_operation_permission_delete_denied() {
840 use crate::config::settings::{
841 FilesystemConfig, OperationConfig, RiskLevel, SecurityPolicy,
842 };
843 use std::collections::HashMap;
844
845 let mut policies = HashMap::new();
847 policies.insert(
848 "no_delete_policy".to_string(),
849 SecurityPolicy {
850 patterns: vec!["**/*".to_string()],
851 operations: vec!["read".to_string(), "write".to_string()], risk_level: RiskLevel::Low,
853 description: None,
854 },
855 );
856
857 let restrictive_config = SecurityConfig {
858 filesystem: FilesystemConfig {
859 allowed_paths: vec!["**/*".to_string()],
860 denied_paths: vec![],
861 },
862 operations: OperationConfig {
863 read_allowed: true,
864 write_requires_policy: false,
865 delete_requires_explicit_allow: true, create_dir_allowed: true,
867 },
868 policies,
869 };
870
871 let manager = SecurityManager::new(restrictive_config).unwrap();
872
873 let operation = FileOperation::new(OperationType::Delete, PathBuf::from("src/main.rs"));
875
876 let result = manager.validate_operation_permission(&operation).await;
877 assert!(result.is_err());
879 let error_msg = result.unwrap_err().to_string();
880 assert!(error_msg.contains("Delete") || error_msg.contains("delete"));
881 }
882
883 #[tokio::test]
884 async fn test_validate_operation_permission_create_dir_success() {
885 let config = create_test_config();
886 let manager = SecurityManager::new(config).unwrap();
887
888 let operation =
889 FileOperation::new(OperationType::CreateDir, PathBuf::from("src/new_module"));
890
891 let result = manager.validate_operation_permission(&operation).await;
892 assert!(result.is_ok());
893 assert_eq!(result.unwrap(), ApprovalDecision::Approved);
894 }
895
896 #[tokio::test]
897 async fn test_validate_operation_permission_list_success() {
898 let config = create_test_config();
899 let manager = SecurityManager::new(config).unwrap();
900
901 let operation = FileOperation::new(OperationType::List, PathBuf::from("src"));
903
904 let result = manager.validate_operation_permission(&operation).await;
905 assert!(result.is_ok());
906 assert_eq!(result.unwrap(), ApprovalDecision::Approved);
907 }
908
909 #[tokio::test]
910 async fn test_validate_operation_permission_move_success() {
911 let config = create_test_config();
912 let manager = SecurityManager::new(config).unwrap();
913
914 let operation = FileOperation::new(OperationType::Move, PathBuf::from("src/old_file.rs"));
915
916 let result = manager.validate_operation_permission(&operation).await;
917 assert!(result.is_ok());
918 assert_eq!(result.unwrap(), ApprovalDecision::Approved);
919 }
920
921 #[tokio::test]
922 async fn test_validate_operation_permission_copy_success() {
923 let config = create_test_config();
924 let manager = SecurityManager::new(config).unwrap();
925
926 let operation = FileOperation::new(OperationType::Copy, PathBuf::from("src/main.rs"));
927
928 let result = manager.validate_operation_permission(&operation).await;
929 assert!(result.is_ok());
930 assert_eq!(result.unwrap(), ApprovalDecision::Approved);
931 }
932
933 #[tokio::test]
934 async fn test_validate_operation_permission_denied_path() {
935 let config = create_test_config();
936 let manager = SecurityManager::new(config).unwrap();
937
938 let operation = FileOperation::new(OperationType::Read, PathBuf::from(".git/config"));
940
941 let result = manager.validate_operation_permission(&operation).await;
942 assert!(result.is_err());
943 let error_msg = result.unwrap_err().to_string();
944 assert!(
946 error_msg.contains("Path validation failed")
947 || error_msg.contains("denied")
948 || error_msg.contains("not allowed")
949 || error_msg.contains("Security policy violation")
950 );
951 }
952
953 #[tokio::test]
954 async fn test_validate_operation_against_policies_write_allowed() {
955 let config = create_test_config();
956 let manager = SecurityManager::new(config).unwrap();
957
958 let operation = FileOperation::new(OperationType::Write, PathBuf::from("src/main.rs"));
959 let correlation_id = CorrelationId::new();
960
961 let result = manager
962 .validate_operation_against_policies(&operation, correlation_id)
963 .await;
964 assert!(result.is_ok());
965 assert!(result.unwrap()); }
967
968 #[tokio::test]
969 async fn test_validate_operation_against_policies_write_denied() {
970 use crate::config::settings::{
971 FilesystemConfig, OperationConfig, RiskLevel, SecurityPolicy,
972 };
973 use std::collections::HashMap;
974
975 let mut policies = HashMap::new();
977 policies.insert(
978 "read_only_policy".to_string(),
979 SecurityPolicy {
980 patterns: vec!["**/*".to_string()],
981 operations: vec!["read".to_string()], risk_level: RiskLevel::Low,
983 description: None,
984 },
985 );
986
987 let restrictive_config = SecurityConfig {
988 filesystem: FilesystemConfig {
989 allowed_paths: vec!["**/*".to_string()],
990 denied_paths: vec![],
991 },
992 operations: OperationConfig {
993 read_allowed: true,
994 write_requires_policy: true, delete_requires_explicit_allow: false,
996 create_dir_allowed: true,
997 },
998 policies,
999 };
1000
1001 let manager = SecurityManager::new(restrictive_config).unwrap();
1002
1003 let operation = FileOperation::new(OperationType::Write, PathBuf::from("test/file.txt"));
1005 let correlation_id = CorrelationId::new();
1006
1007 let result = manager
1008 .validate_operation_against_policies(&operation, correlation_id)
1009 .await;
1010 assert!(result.is_ok());
1011 assert!(!result.unwrap()); }
1013
1014 #[tokio::test]
1015 async fn test_validate_delete_permission_allowed() {
1016 use crate::config::settings::{RiskLevel, SecurityPolicy};
1017
1018 let mut config = create_test_config();
1019
1020 config.policies.insert(
1022 "delete_policy".to_string(),
1023 SecurityPolicy {
1024 patterns: vec!["src/**".to_string()],
1025 operations: vec!["delete".to_string()],
1026 risk_level: RiskLevel::Low,
1027 description: None,
1028 },
1029 );
1030
1031 let manager = SecurityManager::new(config).unwrap();
1032
1033 let operation =
1035 FileOperation::new(OperationType::Delete, PathBuf::from("src/temp_file.txt"));
1036 let correlation_id = CorrelationId::new();
1037
1038 let result = manager
1039 .validate_delete_permission(&operation, correlation_id)
1040 .await;
1041 assert!(result.is_ok());
1042 assert!(result.unwrap()); }
1044
1045 #[tokio::test]
1046 async fn test_validate_delete_permission_denied() {
1047 use crate::config::settings::{
1048 FilesystemConfig, OperationConfig, RiskLevel, SecurityPolicy,
1049 };
1050 use std::collections::HashMap;
1051
1052 let mut policies = HashMap::new();
1054 policies.insert(
1055 "restrictive_policy".to_string(),
1056 SecurityPolicy {
1057 patterns: vec!["**/*".to_string()],
1058 operations: vec!["read".to_string(), "write".to_string()], risk_level: RiskLevel::Low,
1060 description: None,
1061 },
1062 );
1063
1064 let restrictive_config = SecurityConfig {
1065 filesystem: FilesystemConfig {
1066 allowed_paths: vec!["**/*".to_string()],
1067 denied_paths: vec![],
1068 },
1069 operations: OperationConfig {
1070 read_allowed: true,
1071 write_requires_policy: false,
1072 delete_requires_explicit_allow: true, create_dir_allowed: true,
1074 },
1075 policies,
1076 };
1077
1078 let manager = SecurityManager::new(restrictive_config).unwrap();
1079
1080 let operation = FileOperation::new(OperationType::Delete, PathBuf::from("src/main.rs"));
1082 let correlation_id = CorrelationId::new();
1083
1084 let result = manager
1085 .validate_delete_permission(&operation, correlation_id)
1086 .await;
1087 assert!(result.is_ok());
1088 assert!(!result.unwrap()); }
1090
1091 #[tokio::test]
1092 async fn test_operation_type_specific_configuration() {
1093 use crate::config::settings::Settings;
1094
1095 let production_settings = Settings::default();
1097 let production_manager = SecurityManager::new(production_settings.security).unwrap();
1098
1099 assert!(production_manager.config.operations.read_allowed);
1101 assert!(production_manager.config.operations.write_requires_policy);
1102 assert!(
1103 production_manager
1104 .config
1105 .operations
1106 .delete_requires_explicit_allow
1107 );
1108 assert!(production_manager.config.operations.create_dir_allowed);
1109
1110 let permissive_settings = Settings::builder().permissive().build();
1112 let permissive_manager = SecurityManager::new(permissive_settings.security).unwrap();
1113
1114 assert!(permissive_manager.config.operations.read_allowed);
1116 assert!(!permissive_manager.config.operations.write_requires_policy);
1117 assert!(
1118 !permissive_manager
1119 .config
1120 .operations
1121 .delete_requires_explicit_allow
1122 );
1123 assert!(permissive_manager.config.operations.create_dir_allowed);
1124
1125 let restrictive_config = create_test_config();
1127 let restrictive_manager = SecurityManager::new(restrictive_config).unwrap();
1128
1129 assert!(restrictive_manager.config.operations.read_allowed);
1131 assert!(!restrictive_manager.config.operations.write_requires_policy); assert!(
1133 !restrictive_manager
1134 .config
1135 .operations
1136 .delete_requires_explicit_allow
1137 ); assert!(restrictive_manager.config.operations.create_dir_allowed);
1139 }
1140
1141 #[tokio::test]
1143 async fn test_binary_file_rejection_security_hardening() {
1144 let config = create_test_config();
1145 let manager = SecurityManager::new(config).expect("Should create security manager");
1146
1147 let binary_files = vec![
1149 ("/tmp/test.jpg", "JPEG image"),
1150 ("/tmp/test.png", "PNG image"),
1151 ("/tmp/test.gif", "GIF image"),
1152 ("/tmp/test.pdf", "PDF document"),
1153 ("/tmp/test.bmp", "BMP image"),
1154 ("/tmp/test.tiff", "TIFF image"),
1155 ("/tmp/test.webp", "WebP image"),
1156 ];
1157
1158 for (path, description) in binary_files {
1159 let operation = FileOperation::new(OperationType::Read, PathBuf::from(path));
1160
1161 let result = manager.validate_operation_permission(&operation).await;
1162 assert!(
1163 result.is_err(),
1164 "Binary file {description} should be rejected for security"
1165 );
1166
1167 let error_msg = result.unwrap_err().to_string();
1168 assert!(
1169 error_msg.contains("Binary file access denied")
1170 || error_msg.contains("binary")
1171 || error_msg.contains("security"),
1172 "Error message should indicate binary file security restriction: {error_msg}"
1173 );
1174 }
1175 }
1176
1177 #[tokio::test]
1178 async fn test_text_files_allowed_after_binary_hardening() {
1179 let config = create_test_config();
1180 let manager = SecurityManager::new(config).expect("Should create security manager");
1181
1182 let text_files = vec![
1184 "/tmp/test.txt",
1185 "/tmp/test.md",
1186 "/tmp/test.rs",
1187 "/tmp/test.py",
1188 "/tmp/test.js",
1189 "/tmp/test.json",
1190 "/tmp/test.toml",
1191 "/tmp/test.yml",
1192 ];
1193
1194 for path in text_files {
1195 let operation = FileOperation::new(OperationType::Read, PathBuf::from(path));
1196
1197 let result = manager.validate_operation_permission(&operation).await;
1198 if let Err(err) = &result {
1200 let error_msg = err.to_string();
1201 assert!(
1202 !error_msg.contains("Binary file access denied")
1203 && !error_msg.contains("binary")
1204 && !error_msg.contains("Binary"),
1205 "Text file {path} should not trigger binary file restriction: {error_msg}"
1206 );
1207 }
1208 }
1209 }
1210
1211 #[tokio::test]
1212 async fn test_binary_file_content_detection() {
1213 use std::fs;
1214 use tempfile::TempDir;
1215
1216 let config = create_test_config();
1217 let manager = SecurityManager::new(config).expect("Should create security manager");
1218
1219 let temp_dir = TempDir::new().expect("Should create temp dir");
1221
1222 let binary_file = temp_dir.path().join("fake.txt"); let jpeg_header = vec![0xFF, 0xD8, 0xFF, 0xE0]; fs::write(&binary_file, &jpeg_header).expect("Should write binary file");
1226
1227 let operation = FileOperation::new(OperationType::Read, binary_file);
1228
1229 let result = manager.validate_operation_permission(&operation).await;
1230 assert!(
1231 result.is_err(),
1232 "File with binary content should be rejected regardless of extension"
1233 );
1234
1235 let error_msg = result.unwrap_err().to_string();
1236 assert!(
1237 error_msg.contains("Binary file access denied")
1238 || error_msg.contains("content analysis"),
1239 "Error should indicate content-based binary detection: {error_msg}"
1240 );
1241 }
1242}