smart-tree 8.0.1

Smart Tree - An intelligent, AI-friendly directory visualization tool
Documentation
//! Security Vigilance Mode - Smart Tree as your security sentinel! 🕵️‍♂️
//! 
//! Always watching for anomalies, even in the "boring" places where bad actors hide.
//! Takes random samples, tracks recent modifications, and looks for suspicious patterns.

use crate::scanner::FileNode;
use chrono::{DateTime, Local, Duration};
use rand::{thread_rng, Rng};
use std::collections::{HashMap, VecDeque};
use std::fs;
use std::io::Read;
use std::path::{Path, PathBuf};

/// Security alert levels
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum AlertLevel {
    /// 🟢 Normal - Nothing suspicious
    Normal,
    /// 🟡 Interesting - Worth noting
    Interesting,
    /// 🟠 Suspicious - Potential issue
    Suspicious,
    /// 🔴 Critical - Definite security concern
    Critical,
}

impl AlertLevel {
    pub fn emoji(&self) -> &'static str {
        match self {
            Self::Normal => "🟢",
            Self::Interesting => "🟡",
            Self::Suspicious => "🟠",
            Self::Critical => "🔴",
        }
    }
}

/// Security finding
#[derive(Debug, Clone)]
pub struct SecurityFinding {
    pub path: PathBuf,
    pub alert_level: AlertLevel,
    pub reason: String,
    pub details: Option<String>,
    pub timestamp: DateTime<Local>,
}

/// Tracks recent file modifications
#[derive(Debug)]
pub struct RecentWriteTracker {
    /// Last N modified files per directory
    recent_writes: HashMap<PathBuf, VecDeque<(PathBuf, DateTime<Local>)>>,
    /// How many recent writes to track
    track_count: usize,
}

impl RecentWriteTracker {
    pub fn new(track_count: usize) -> Self {
        Self {
            recent_writes: HashMap::new(),
            track_count,
        }
    }
    
    pub fn add_file(&mut self, dir: &Path, file: &Path, modified: DateTime<Local>) {
        let writes = self.recent_writes
            .entry(dir.to_path_buf())
            .or_insert_with(|| VecDeque::with_capacity(self.track_count));
        
        writes.push_front((file.to_path_buf(), modified));
        if writes.len() > self.track_count {
            writes.pop_back();
        }
    }
    
    pub fn get_recent_writes(&self, dir: &Path) -> Vec<(PathBuf, DateTime<Local>)> {
        self.recent_writes
            .get(dir)
            .map(|deque| deque.iter().cloned().collect())
            .unwrap_or_default()
    }
}

/// Security vigilance analyzer
pub struct SecurityVigilance {
    /// Track recent writes
    write_tracker: RecentWriteTracker,
    
    /// Security findings
    findings: Vec<SecurityFinding>,
    
    /// Suspicious patterns to look for
    suspicious_patterns: Vec<(regex::Regex, String, AlertLevel)>,
    
    /// Suspicious file names
    suspicious_names: HashMap<String, (String, AlertLevel)>,
    
    /// Allowed sample size for file inspection
    max_sample_size: usize,
    
    /// Directories that should rarely change
    protected_paths: Vec<String>,
}

impl SecurityVigilance {
    pub fn new() -> Self {
        let mut suspicious_patterns = vec![];
        let mut suspicious_names = HashMap::new();
        
        // Suspicious content patterns
        suspicious_patterns.push((
            regex::Regex::new(r"eval\s*\(|exec\s*\(").unwrap(),
            "Dynamic code execution detected".to_string(),
            AlertLevel::Suspicious,
        ));
        
        suspicious_patterns.push((
            regex::Regex::new(r"(?i)(password|passwd|pwd)\s*=\s*[\"'][^\"']+[\"']").unwrap(),
            "Hardcoded password detected".to_string(),
            AlertLevel::Critical,
        ));
        
        suspicious_patterns.push((
            regex::Regex::new(r"(?i)api[_-]?key\s*=\s*[\"'][^\"']+[\"']").unwrap(),
            "Hardcoded API key detected".to_string(),
            AlertLevel::Critical,
        ));
        
        suspicious_patterns.push((
            regex::Regex::new(r"0x[0-9a-fA-F]{40,}").unwrap(),
            "Possible crypto wallet address".to_string(),
            AlertLevel::Interesting,
        ));
        
        suspicious_patterns.push((
            regex::Regex::new(r"(?i)wget|curl.*http").unwrap(),
            "Network download command detected".to_string(),
            AlertLevel::Suspicious,
        ));
        
        suspicious_patterns.push((
            regex::Regex::new(r"/etc/passwd|/etc/shadow").unwrap(),
            "System file access detected".to_string(),
            AlertLevel::Critical,
        ));
        
        // Suspicious file names
        suspicious_names.insert(".env.prod".to_string(), 
            ("Production environment file".to_string(), AlertLevel::Suspicious));
        suspicious_names.insert("id_rsa".to_string(), 
            ("Private SSH key".to_string(), AlertLevel::Critical));
        suspicious_names.insert(".npmrc".to_string(), 
            ("NPM configuration with possible tokens".to_string(), AlertLevel::Interesting));
        suspicious_names.insert("wallet.dat".to_string(), 
            ("Cryptocurrency wallet file".to_string(), AlertLevel::Critical));
        
        // Backdoor-ish names
        suspicious_names.insert("backdoor.js".to_string(), 
            ("Suspicious filename: backdoor".to_string(), AlertLevel::Critical));
        suspicious_names.insert("shell.php".to_string(), 
            ("Web shell detected".to_string(), AlertLevel::Critical));
        suspicious_names.insert("c99.php".to_string(), 
            ("Known web shell name".to_string(), AlertLevel::Critical));
        
        let protected_paths = vec![
            "node_modules".to_string(),
            ".git".to_string(),
            "System32".to_string(),
            "/etc".to_string(),
            "/usr/bin".to_string(),
            "/usr/local/bin".to_string(),
        ];
        
        Self {
            write_tracker: RecentWriteTracker::new(5),
            findings: Vec::new(),
            suspicious_patterns,
            suspicious_names,
            max_sample_size: 1024, // 1KB sample
            protected_paths,
        }
    }
    
    /// Analyze a file node for security issues
    pub fn analyze_node(&mut self, node: &FileNode, parent_path: &Path) {
        let full_path = parent_path.join(&node.name);
        
        // Check if this is a recent write
        if let Ok(metadata) = fs::metadata(&full_path) {
            if let Ok(modified) = metadata.modified() {
                let modified_time: DateTime<Local> = modified.into();
                let now = Local::now();
                
                // Track if modified in last 24 hours
                if now.signed_duration_since(modified_time) < Duration::hours(24) {
                    self.write_tracker.add_file(parent_path, &full_path, modified_time);
                    
                    // Extra vigilance for recent writes in protected paths
                    if self.is_protected_path(&full_path) {
                        self.add_finding(SecurityFinding {
                            path: full_path.clone(),
                            alert_level: AlertLevel::Interesting,
                            reason: "Recent modification in protected directory".to_string(),
                            details: Some(format!("Modified: {}", modified_time.format("%Y-%m-%d %H:%M:%S"))),
                            timestamp: now,
                        });
                    }
                }
            }
        }
        
        // Check suspicious file names
        if let Some((reason, level)) = self.suspicious_names.get(&node.name.to_lowercase()) {
            self.add_finding(SecurityFinding {
                path: full_path.clone(),
                alert_level: *level,
                reason: reason.clone(),
                details: None,
                timestamp: Local::now(),
            });
        }
        
        // Sample file content for suspicious patterns
        if !node.is_directory && node.size > 0 {
            self.sample_file_content(&full_path);
        }
    }
    
    /// Take a random sample from a file and check for suspicious patterns
    fn sample_file_content(&mut self, path: &Path) {
        // Skip binary files (simple heuristic based on extension)
        if let Some(ext) = path.extension() {
            let ext_str = ext.to_string_lossy().to_lowercase();
            if matches!(ext_str.as_str(), "exe" | "dll" | "so" | "dylib" | "bin" | "jpg" | "png" | "mp4") {
                return;
            }
        }
        
        // Try to read a sample
        if let Ok(mut file) = fs::File::open(path) {
            let file_size = file.metadata().map(|m| m.len()).unwrap_or(0);
            
            if file_size > 0 {
                let mut buffer = vec![0u8; self.max_sample_size.min(file_size as usize)];
                
                // Random offset for larger files
                if file_size > self.max_sample_size as u64 {
                    let max_offset = file_size - self.max_sample_size as u64;
                    let offset = thread_rng().gen_range(0..max_offset);
                    let _ = file.seek(std::io::SeekFrom::Start(offset));
                }
                
                if let Ok(bytes_read) = file.read(&mut buffer) {
                    buffer.truncate(bytes_read);
                    
                    // Convert to string (lossy for non-UTF8)
                    let content = String::from_utf8_lossy(&buffer);
                    
                    // Check patterns
                    for (pattern, reason, level) in &self.suspicious_patterns {
                        if pattern.is_match(&content) {
                            self.add_finding(SecurityFinding {
                                path: path.to_path_buf(),
                                alert_level: *level,
                                reason: reason.clone(),
                                details: Some("Found in random sample".to_string()),
                                timestamp: Local::now(),
                            });
                        }
                    }
                }
            }
        }
    }
    
    /// Check if a path is in a protected directory
    fn is_protected_path(&self, path: &Path) -> bool {
        let path_str = path.to_string_lossy().to_lowercase();
        self.protected_paths.iter().any(|protected| {
            path_str.contains(&protected.to_lowercase())
        })
    }
    
    /// Add a security finding
    pub fn add_finding(&mut self, finding: SecurityFinding) {
        self.findings.push(finding);
    }
    
    /// Get all findings sorted by severity
    pub fn get_findings(&self) -> Vec<&SecurityFinding> {
        let mut findings: Vec<_> = self.findings.iter().collect();
        findings.sort_by_key(|f| std::cmp::Reverse(f.alert_level));
        findings
    }
    
    /// Get summary of findings
    pub fn summary(&self) -> String {
        let mut summary = String::from("🕵️ Security Vigilance Report\n\n");
        
        let mut counts = HashMap::new();
        for finding in &self.findings {
            *counts.entry(finding.alert_level).or_insert(0) += 1;
        }
        
        if counts.is_empty() {
            summary.push_str("✅ No security issues detected!\n");
        } else {
            summary.push_str("Findings by severity:\n");
            for level in [AlertLevel::Critical, AlertLevel::Suspicious, AlertLevel::Interesting, AlertLevel::Normal] {
                if let Some(count) = counts.get(&level) {
                    summary.push_str(&format!("  {} {} findings\n", level.emoji(), count));
                }
            }
            
            // Show critical findings
            summary.push_str("\n");
            for finding in self.findings.iter().filter(|f| f.alert_level == AlertLevel::Critical) {
                summary.push_str(&format!(
                    "{} {} - {}\n", 
                    finding.alert_level.emoji(),
                    finding.path.display(),
                    finding.reason
                ));
            }
        }
        
        summary
    }
}

// Vigilance patterns that could be in AI modes:
//
// 1. Always show last 5 modified files in each directory
// 2. Random sampling from files to detect:
//    - Hardcoded secrets
//    - Malicious code patterns
//    - Suspicious network calls
//    - Backdoors
// 3. Track modifications in "boring" directories like node_modules
// 4. Alert on files that shouldn't exist (like .env.prod in git)
// 5. Detect anomalies in system directories

// Trisha says: "It's like having a security guard who actually checks 
// the boring supply closets where people hide things! Smart!" 🔍