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::git::dangerous::DANGEROUS_COMPONENTS;
use crate::plugins::traits::{PluginError, PluginReport, ScanContext, ScanPhase, SecurityPlugin};
use async_trait::async_trait;
use std::path::Path;
use std::time::Instant;

pub struct GitInternalsScanner;

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

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

    fn scan_for_hooks(&self, git_dir: &Path) -> Vec<Finding> {
        let mut findings = Vec::new();
        let hooks_dir = git_dir.join("hooks");

        if !hooks_dir.exists() {
            return findings;
        }

        if let Ok(entries) = std::fs::read_dir(&hooks_dir) {
            for entry in entries.flatten() {
                let path = entry.path();
                if path.is_file() {
                    let name = path
                        .file_name()
                        .and_then(|n| n.to_str())
                        .unwrap_or("unknown");

                    // Check if it's a dangerous hook that should have been removed
                    if DANGEROUS_COMPONENTS.hooks.contains(&name) {
                        findings.push(
                            Finding::new(
                                format!("GIT-HOOK-{}", name.to_uppercase()),
                                format!("Dangerous git hook found: {}", name),
                                Severity::Critical,
                            )
                            .with_file(path.clone())
                            .with_description(format!(
                                "Git hook '{}' was found after sanitization. \
                                 This hook can execute arbitrary code and should be removed.",
                                name
                            )),
                        );
                    }
                }
            }
        }

        findings
    }

    fn scan_config(&self, git_dir: &Path) -> Vec<Finding> {
        let mut findings = Vec::new();
        let config_path = git_dir.join("config");

        if !config_path.exists() {
            return findings;
        }

        if let Ok(content) = std::fs::read_to_string(&config_path) {
            for (line_num, line) in content.lines().enumerate() {
                let trimmed = line.trim();

                // Check for dangerous config keys
                for dangerous_key in DANGEROUS_COMPONENTS.dangerous_config_keys {
                    let key_pattern = dangerous_key.replace("*", "");
                    if trimmed.to_lowercase().contains(&key_pattern.to_lowercase()) {
                        findings.push(
                            Finding::new(
                                format!("GIT-CFG-{:03}", line_num),
                                format!("Dangerous git config key: {}", dangerous_key),
                                Severity::High,
                            )
                            .with_file(config_path.clone())
                            .with_line((line_num + 1) as u32)
                            .with_evidence(trimmed.to_string())
                            .with_description(format!(
                                "Dangerous git config key '{}' found. \
                                 This can be used to execute arbitrary code.",
                                dangerous_key
                            )),
                        );
                    }
                }
            }
        }

        findings
    }
}

#[async_trait]
impl SecurityPlugin for GitInternalsScanner {
    fn name(&self) -> &str {
        "git-internals"
    }

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

    fn description(&self) -> &str {
        "Scan .git directory for threats and verify sanitization"
    }

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

    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());

        // Check if this is a .git directory
        let git_dir_buf;
        let git_dir = if context.path.ends_with(".git") {
            context.path
        } else {
            git_dir_buf = context.path.join(".git");
            git_dir_buf.as_path()
        };

        if git_dir.exists() {
            let mut all_findings = Vec::new();

            all_findings.extend(self.scan_for_hooks(git_dir));
            all_findings.extend(self.scan_config(git_dir));

            report.findings = all_findings;
            report.scanned_files = 1;
        }

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