securegit 0.8.5

Zero-trust git replacement with 12 built-in security scanners, LLM redteam bridge, universal undo, durable backups, and a 50-tool MCP server
Documentation
use crate::core::{Finding, Severity};
use crate::plugins::traits::{PluginError, PluginReport, ScanContext, ScanPhase, SecurityPlugin};
use async_trait::async_trait;
use lazy_static::lazy_static;
use regex::Regex;
use std::time::Instant;

lazy_static! {
    static ref PATTERNS: Vec<(Regex, &'static str, Severity)> = vec![
        (
            Regex::new(r"eval\s*\(").unwrap(),
            "Dangerous eval() usage",
            Severity::High
        ),
        (
            Regex::new(r"exec\s*\(").unwrap(),
            "Dangerous exec() usage",
            Severity::High
        ),
        (
            Regex::new(r"system\s*\(").unwrap(),
            "Shell command execution",
            Severity::Medium
        ),
        (
            Regex::new(r#"(?i)password\s*=\s*['"][^'"]+['"]"#).unwrap(),
            "Hardcoded password",
            Severity::Critical
        ),
        (
            Regex::new(r#"(?i)api[_-]?key\s*=\s*['"][^'"]+['"]"#).unwrap(),
            "Hardcoded API key",
            Severity::Critical
        ),
        (
            Regex::new(r#"(?i)secret\s*=\s*['"][^'"]+['"]"#).unwrap(),
            "Hardcoded secret",
            Severity::High
        ),
        (
            Regex::new(r"(?i)(bitcoin|btc)[_-]?(private[_-]?key|privkey)").unwrap(),
            "Crypto private key",
            Severity::Critical
        ),
        (
            Regex::new(r"/bin/(sh|bash|zsh|fish)\s+-c").unwrap(),
            "Shell command injection risk",
            Severity::High
        ),
        (
            Regex::new(r"(?i)backdoor").unwrap(),
            "Potential backdoor",
            Severity::Critical
        ),
        (
            Regex::new(r"(?i)(reverse|bind)[_-]?shell").unwrap(),
            "Reverse/bind shell",
            Severity::Critical
        ),
        (
            Regex::new(r"base64[_-]?decode").unwrap(),
            "Base64 decode (potential obfuscation)",
            Severity::Low
        ),
        (
            Regex::new(r"0x[0-9a-fA-F]{40,}").unwrap(),
            "Long hex string (potential obfuscation)",
            Severity::Low
        ),
    ];
}

pub struct PatternScanner;

impl Default for PatternScanner {
    fn default() -> Self {
        Self::new()
    }
}

impl PatternScanner {
    pub fn new() -> Self {
        Self
    }
}

#[async_trait]
impl SecurityPlugin for PatternScanner {
    fn name(&self) -> &str {
        "patterns"
    }

    fn version(&self) -> &str {
        "0.1.0"
    }

    fn description(&self) -> &str {
        "Pattern-based threat detection using regex"
    }

    fn scan_phase(&self) -> ScanPhase {
        ScanPhase::All
    }

    async fn initialize(&mut self) -> Result<(), PluginError> {
        Ok(())
    }

    async fn scan(&self, context: &ScanContext<'_>) -> Result<PluginReport, PluginError> {
        let start = Instant::now();
        let mut report = PluginReport::new(self.name().to_string());

        if let Some(content) = context.file_content {
            let content_str = String::from_utf8_lossy(content);

            for (pattern, description, severity) in PATTERNS.iter() {
                for (line_num, line) in content_str.lines().enumerate() {
                    if pattern.is_match(line) {
                        let finding = Finding::new(
                            format!("PAT-{:03}", line_num),
                            description.to_string(),
                            *severity,
                        )
                        .with_file(context.path.to_path_buf())
                        .with_line((line_num + 1) as u32)
                        .with_evidence(line.to_string());

                        report.findings.push(finding);
                    }
                }
            }

            report.scanned_files = 1;
        }

        report.duration_ms = start.elapsed().as_millis() as u64;
        Ok(report)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::plugins::traits::ScanContext;

    #[tokio::test]
    async fn test_eval_detection() {
        let scanner = PatternScanner::new();
        let content = b"eval(user_input)";
        let context = ScanContext {
            path: std::path::Path::new("test.txt"),
            scan_phase: ScanPhase::PostExtract,
            file_content: Some(content),
            metadata: std::collections::HashMap::new(),
        };
        let report = scanner.scan(&context).await.unwrap();
        assert!(!report.findings.is_empty());
    }

    #[tokio::test]
    async fn test_hardcoded_password() {
        let scanner = PatternScanner::new();
        let content = br#"password = "s3cret""#;
        let context = ScanContext {
            path: std::path::Path::new("test.txt"),
            scan_phase: ScanPhase::PostExtract,
            file_content: Some(content),
            metadata: std::collections::HashMap::new(),
        };
        let report = scanner.scan(&context).await.unwrap();
        assert!(!report.findings.is_empty());
    }

    #[tokio::test]
    async fn test_reverse_shell() {
        let scanner = PatternScanner::new();
        let content = b"reverse_shell";
        let context = ScanContext {
            path: std::path::Path::new("test.txt"),
            scan_phase: ScanPhase::PostExtract,
            file_content: Some(content),
            metadata: std::collections::HashMap::new(),
        };
        let report = scanner.scan(&context).await.unwrap();
        assert!(!report.findings.is_empty());
    }

    #[tokio::test]
    async fn test_clean_code() {
        let scanner = PatternScanner::new();
        let content = b"let x = 42;";
        let context = ScanContext {
            path: std::path::Path::new("test.txt"),
            scan_phase: ScanPhase::PostExtract,
            file_content: Some(content),
            metadata: std::collections::HashMap::new(),
        };
        let report = scanner.scan(&context).await.unwrap();
        assert_eq!(report.findings.len(), 0);
    }
}