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 = "modelscan";

pub struct ModelScanBridge {
    bridge: CliToolBridge,
}

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

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

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

    /// Scan a directory or file for malicious model files.
    pub async fn scan(&self, path: &Path) -> Result<Vec<Finding>, CliError> {
        let path_str = path.to_string_lossy();
        let (exit_code, stdout, stderr) = self
            .bridge
            .run(&["scan", "-p", &path_str, "--json"])
            .await?;

        // Exit code 3 = no supported files found (not an error)
        if exit_code == 3 {
            return Ok(Vec::new());
        }
        // Exit code 2 = error
        if exit_code == 2 {
            return Err(CliError::CallFailed(format!("modelscan error: {}", stderr)));
        }

        Ok(parse_modelscan_output(&stdout))
    }
}

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

    let issues = match value.get("issues").and_then(|v| v.as_array()) {
        Some(arr) => arr,
        None => return Vec::new(),
    };

    issues
        .iter()
        .map(|issue| {
            let severity_str = issue
                .get("severity")
                .and_then(|v| v.as_str())
                .unwrap_or("medium");
            let severity = match severity_str.to_lowercase().as_str() {
                "critical" | "cri" => Severity::Critical,
                "high" | "hig" => Severity::High,
                "medium" | "med" => Severity::Medium,
                "low" => Severity::Low,
                _ => Severity::Medium,
            };

            let code = issue
                .get("code")
                .and_then(|v| v.as_str())
                .unwrap_or("unknown");
            let description = issue
                .get("description")
                .and_then(|v| v.as_str())
                .unwrap_or("");
            let source = issue.get("source").and_then(|v| v.as_str());

            let mut finding = Finding::new(
                format!("modelscan-{}", code),
                format!("ModelScan: {}", description),
                severity,
            );
            finding.confidence = Confidence::High;
            finding.description = description.to_string();
            finding.cwe_ids.push(502); // CWE-502: Deserialization of Untrusted Data
            finding.tags.push("model-security".to_string());
            finding.tags.push("deserialization".to_string());

            if let Some(src) = source {
                finding.file_path = Some(std::path::PathBuf::from(src));
            }

            finding
        })
        .collect()
}