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 super::{CliError, CliToolBridge};
use crate::core::{Confidence, Finding, Severity};
use std::path::Path;

const BINARY: &str = "uvx";

pub struct McpScanBridge {
    bridge: CliToolBridge,
}

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

impl McpScanBridge {
    pub fn new() -> Self {
        Self {
            bridge: CliToolBridge::new(BINARY),
        }
    }

    pub fn is_available(&self) -> bool {
        self.bridge.is_available()
    }

    /// Scan an MCP config file for security issues.
    pub async fn scan(&self, config_path: &Path) -> Result<Vec<Finding>, CliError> {
        let path_str = config_path.to_string_lossy();
        let (_exit_code, stdout, _stderr) = self
            .bridge
            .run(&["mcp-scan@latest", "scan", &path_str])
            .await?;

        Ok(parse_mcp_scan_output(&stdout, config_path))
    }
}

/// Parse mcp-scan JSON output into Findings.
pub fn parse_mcp_scan_output(json_text: &str, config_path: &Path) -> Vec<Finding> {
    let value: serde_json::Value = match serde_json::from_str(json_text) {
        Ok(v) => v,
        Err(_) => return Vec::new(),
    };

    let findings_array = value
        .get("findings")
        .and_then(|v| v.as_array())
        .or_else(|| value.as_array());

    let Some(items) = findings_array else {
        return Vec::new();
    };

    items
        .iter()
        .map(|item| {
            let issue_type = item
                .get("type")
                .or_else(|| item.get("issue_type"))
                .and_then(|v| v.as_str())
                .unwrap_or("unknown");

            let title = item
                .get("title")
                .or_else(|| item.get("message"))
                .and_then(|v| v.as_str())
                .unwrap_or(issue_type);

            let severity_str = item
                .get("severity")
                .and_then(|v| v.as_str())
                .unwrap_or("medium");

            let severity = match severity_str.to_lowercase().as_str() {
                "critical" => Severity::Critical,
                "high" => Severity::High,
                "medium" => Severity::Medium,
                "low" => Severity::Low,
                _ => Severity::Medium,
            };

            let mut finding = Finding::new(
                format!("mcp-scan-{}", issue_type.to_lowercase().replace(' ', "-")),
                format!("mcp-scan: {}", title),
                severity,
            );
            finding.confidence = Confidence::Medium;
            finding.file_path = Some(config_path.to_path_buf());
            finding.tags.push("mcp-security".to_string());
            finding.tags.push(issue_type.to_lowercase());

            if let Some(desc) = item.get("description").and_then(|v| v.as_str()) {
                finding.description = desc.to_string();
            }

            if let Some(rem) = item.get("remediation").and_then(|v| v.as_str()) {
                finding.remediation = Some(rem.to_string());
            }

            // Tool name as evidence
            if let Some(tool) = item.get("tool_name").and_then(|v| v.as_str()) {
                finding.evidence.push(format!("Tool: {}", tool));
            }
            if let Some(server) = item.get("server_name").and_then(|v| v.as_str()) {
                finding.evidence.push(format!("Server: {}", server));
            }

            finding
        })
        .collect()
}