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::Severity;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;

#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct Config {
    pub general: GeneralConfig,
    pub acquisition: AcquisitionConfig,
    pub sanitization: SanitizeConfig,
    pub archive: ArchiveConfig,
    pub scan: ScanConfig,
    pub plugins: PluginsConfig,
    pub network: NetworkConfig,
    pub output: OutputConfig,
    pub hooks: HooksConfig,
    pub auth: AuthConfig,
    pub submodules: SubmodulesConfig,
    pub lfs: LfsConfig,
    pub proxy: ProxyConfig,
    pub llm_security: LlmSecurityConfig,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct GeneralConfig {
    pub quarantine_dir: PathBuf,
    pub parallel: bool,
    pub max_threads: usize,
    pub fail_on_severity: Severity,
    pub file_timeout_seconds: u64,
    pub total_timeout_seconds: u64,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct AcquisitionConfig {
    pub default_strategy: String,
    pub max_download_size_mb: u64,
    pub verify_integrity: bool,
    pub use_gitoxide: bool,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct SanitizeConfig {
    pub remove_hooks: bool,
    pub sanitize_config: bool,
    pub sanitize_attributes: bool,
    pub disable_lfs: bool,
    pub remove_submodules: bool,
    pub allowed_config_keys: Vec<String>,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ArchiveConfig {
    pub max_compression_ratio: u64,
    pub max_extracted_size_mb: u64,
    pub max_file_count: usize,
    pub max_nesting_depth: usize,
    pub max_single_file_mb: u64,
    pub max_path_length: usize,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ScanConfig {
    pub skip_paths: Vec<String>,
    pub force_scan: Vec<String>,
    pub max_scan_file_mb: u64,
    pub allowlist_files: Vec<String>,
}

impl ScanConfig {
    /// Check if a file path matches any allowlist glob pattern.
    pub fn is_allowlisted(&self, path: &std::path::Path) -> bool {
        let path_str = path.to_string_lossy();
        self.allowlist_files.iter().any(|pat| {
            glob::Pattern::new(pat)
                .map(|g| g.matches(&path_str))
                .unwrap_or(false)
        })
    }
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct PluginsConfig {
    pub enabled: Vec<String>,
    pub plugin_dir: PathBuf,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct NetworkConfig {
    pub offline: bool,
    pub proxy: String,
    pub timeout_seconds: u64,
    pub retries: u32,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct OutputConfig {
    pub format: String,
    pub report_dir: PathBuf,
    pub keep_reports_days: u32,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct AuthConfig {
    pub prefer_ssh: bool,
    pub token_sources: Vec<String>,
    pub ssh_key_path: Option<PathBuf>,
    pub use_credential_helper: bool,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct SubmodulesConfig {
    pub auto_acquire: bool,
    pub max_depth: usize,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct LfsConfig {
    pub enabled: bool,
    pub verify_hashes: bool,
    pub max_object_size_mb: u64,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ProxyConfig {
    pub scan_on_pull: bool,
    pub scan_on_merge: bool,
    pub block_push_on_secrets: bool,
    pub incremental_scan: bool,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct LlmSecurityConfig {
    pub enabled: bool,
    /// Path to armyknife-llm-redteam-mcp binary. Auto-detected from PATH if omitted.
    /// Override with SECUREGIT_REDTEAM_BIN env var.
    pub binary: Option<String>,
    pub scan_on_acquire: bool,
    pub scan_mcp_configs: bool,
    pub firewall_prompts: bool,
    pub verify_pins: bool,
    pub fail_on_severity: Option<Severity>,
    pub mcp_config_patterns: Vec<String>,
    pub run_modelscan: bool,
    pub run_picklescan: bool,
    pub run_fickling: bool,
    pub run_mcp_scan: bool,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct HooksConfig {
    pub pre_commit_enabled: bool,
    pub pre_push_enabled: bool,
    pub fail_on_severity: Severity,
    pub auto_fix: bool,
}

impl Default for GeneralConfig {
    fn default() -> Self {
        // Use a user-specific cache directory instead of a predictable /tmp path
        let quarantine_dir = dirs::cache_dir()
            .map(|d| d.join("securegit").join("quarantine"))
            .unwrap_or_else(|| {
                dirs::home_dir()
                    .map(|h| h.join(".cache/securegit/quarantine"))
                    .unwrap_or_else(|| PathBuf::from("/tmp/securegit-quarantine"))
            });
        Self {
            quarantine_dir,
            parallel: true,
            max_threads: 0,
            fail_on_severity: Severity::High,
            file_timeout_seconds: 30,
            total_timeout_seconds: 600,
        }
    }
}

impl Default for AcquisitionConfig {
    fn default() -> Self {
        Self {
            default_strategy: "zip-with-history".to_string(),
            max_download_size_mb: 500,
            verify_integrity: true,
            use_gitoxide: true,
        }
    }
}

impl Default for SanitizeConfig {
    fn default() -> Self {
        Self {
            remove_hooks: true,
            sanitize_config: true,
            sanitize_attributes: true,
            disable_lfs: true,
            remove_submodules: false,
            allowed_config_keys: vec![
                "core.repositoryformatversion".to_string(),
                "core.filemode".to_string(),
                "core.bare".to_string(),
                "core.logallrefupdates".to_string(),
                "core.ignorecase".to_string(),
                "core.autocrlf".to_string(),
                "core.symlinks".to_string(),
                "core.precomposeunicode".to_string(),
                "user.name".to_string(),
                "user.email".to_string(),
                "remote.*".to_string(),
                "branch.*".to_string(),
            ],
        }
    }
}

impl Default for ArchiveConfig {
    fn default() -> Self {
        Self {
            max_compression_ratio: 100,
            max_extracted_size_mb: 5000,
            max_file_count: 50000,
            max_nesting_depth: 25,
            max_single_file_mb: 1000,
            max_path_length: 4096,
        }
    }
}

impl Default for ScanConfig {
    fn default() -> Self {
        Self {
            skip_paths: vec![
                "node_modules/**".to_string(),
                "vendor/**".to_string(),
                "__pycache__/**".to_string(),
            ],
            force_scan: vec!["**/package.json".to_string(), "**/Cargo.toml".to_string()],
            max_scan_file_mb: 50,
            allowlist_files: vec![],
        }
    }
}

impl Default for PluginsConfig {
    fn default() -> Self {
        let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
        Self {
            enabled: vec![
                "patterns".to_string(),
                "entropy".to_string(),
                "secrets".to_string(),
                "binary".to_string(),
            ],
            plugin_dir: home.join(".config/securegit/plugins"),
        }
    }
}

impl Default for NetworkConfig {
    fn default() -> Self {
        Self {
            offline: false,
            proxy: String::new(),
            timeout_seconds: 30,
            retries: 3,
        }
    }
}

impl Default for OutputConfig {
    fn default() -> Self {
        let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
        Self {
            format: "pretty".to_string(),
            report_dir: home.join(".local/share/securegit/reports"),
            keep_reports_days: 30,
        }
    }
}

impl Default for HooksConfig {
    fn default() -> Self {
        Self {
            pre_commit_enabled: true,
            pre_push_enabled: true,
            fail_on_severity: Severity::High,
            auto_fix: false,
        }
    }
}

impl Default for AuthConfig {
    fn default() -> Self {
        Self {
            prefer_ssh: false,
            token_sources: vec![
                "SECUREGIT_TOKEN".to_string(),
                "GITHUB_TOKEN".to_string(),
                "GH_TOKEN".to_string(),
                "GITLAB_TOKEN".to_string(),
            ],
            ssh_key_path: None,
            use_credential_helper: true,
        }
    }
}

impl Default for SubmodulesConfig {
    fn default() -> Self {
        Self {
            auto_acquire: false,
            max_depth: 10,
        }
    }
}

impl Default for LfsConfig {
    fn default() -> Self {
        Self {
            enabled: false,
            verify_hashes: true,
            max_object_size_mb: 500,
        }
    }
}

impl Default for ProxyConfig {
    fn default() -> Self {
        Self {
            scan_on_pull: true,
            scan_on_merge: true,
            block_push_on_secrets: true,
            incremental_scan: true,
        }
    }
}

impl Default for LlmSecurityConfig {
    fn default() -> Self {
        Self {
            enabled: false,
            binary: None,
            scan_on_acquire: true,
            scan_mcp_configs: true,
            firewall_prompts: true,
            verify_pins: true,
            fail_on_severity: None,
            mcp_config_patterns: vec![
                "claude_desktop_config.json".into(),
                "mcp.json".into(),
                ".claude/settings.json".into(),
                ".cursor/mcp.json".into(),
                "cline_mcp_settings.json".into(),
            ],
            run_modelscan: true,
            run_picklescan: true,
            run_fickling: true,
            run_mcp_scan: true,
        }
    }
}