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 SECRET_PATTERNS: Vec<(Regex, &'static str, Severity)> = vec![
        (
            Regex::new(r"(?i)AKIA[0-9A-Z]{16}").unwrap(),
            "AWS_Access_Key_ID",
            Severity::Critical
        ),
        (
            Regex::new(r"(?i)aws[_-]?secret[_-]?access[_-]?key").unwrap(),
            "AWS_Secret_Key",
            Severity::Critical
        ),
        (
            Regex::new(r"(?i)github[_-]?token").unwrap(),
            "GitHub_Token",
            Severity::High
        ),
        (
            Regex::new(r"ghp_[0-9a-zA-Z]{36}").unwrap(),
            "GitHub_Personal_Access_Token",
            Severity::Critical
        ),
        (
            Regex::new(r"gho_[0-9a-zA-Z]{36}").unwrap(),
            "GitHub_OAuth_Token",
            Severity::Critical
        ),
        (
            Regex::new(r"(?i)slack[_-]?token").unwrap(),
            "Slack_Token",
            Severity::High
        ),
        (
            Regex::new(r"xox[baprs]-[0-9a-zA-Z-]+").unwrap(),
            "Slack_API_Token",
            Severity::Critical
        ),
        (
            Regex::new(r"(?i)private[_-]?key").unwrap(),
            "Private_Key_Reference",
            Severity::High
        ),
        (
            Regex::new(r"-----BEGIN (RSA |EC )?PRIVATE KEY-----").unwrap(),
            "Private_Key",
            Severity::Critical
        ),
        (
            Regex::new(r"sk-[0-9a-zA-Z]{48}").unwrap(),
            "OpenAI_API_Key",
            Severity::Critical
        ),
        (
            Regex::new(r"AIza[0-9A-Za-z-_]{35}").unwrap(),
            "Google_API_Key",
            Severity::Critical
        ),
        (
            Regex::new(r"(?i)db[_-]?password").unwrap(),
            "Database_Password",
            Severity::High
        ),
    ];
}

pub struct SecretsScanner;

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

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

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

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

    fn description(&self) -> &str {
        "Detect_exposed_credentials_and_API_keys"
    }

    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 (line_num, line) in content_str.lines().enumerate() {
                for (pattern, description, severity) in SECRET_PATTERNS.iter() {
                    if pattern.is_match(line) {
                        let redacted_line = redact_secrets(line);
                        let finding = Finding::new(
                            format!("SEC-{:03}", line_num),
                            format!("Found: {}", description.replace('_', " ")),
                            *severity,
                        )
                        .with_file(context.path.to_path_buf())
                        .with_line((line_num + 1) as u32)
                        .with_evidence(redacted_line)
                        .with_description(format!(
                            "Potential {} found in source code. This should be removed and rotated.",
                            description.replace('_', " ")
                        ));

                        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_aws_key_detection() {
        let scanner = SecretsScanner::new();
        let content = b"AKIAIOSFODNN7EXAMPLE";
        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_github_pat_detection() {
        let scanner = SecretsScanner::new();
        let content = b"ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghij";
        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_private_key_detection() {
        let scanner = SecretsScanner::new();
        let content = b"-----BEGIN RSA PRIVATE KEY-----";
        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_file() {
        let scanner = SecretsScanner::new();
        let content = b"fn main() { println!(\"hello\"); }";
        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);
    }
}

fn redact_secrets(line: &str) -> String {
    let mut redacted = line.to_string();

    // Redact anything that looks like a key/token
    let redact_pattern = Regex::new(r"[A-Za-z0-9+/=]{20,}").unwrap();
    redacted = redact_pattern
        .replace_all(&redacted, "REDACTED")
        .to_string();

    redacted
}