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::{Confidence, Finding, Severity};
use std::path::PathBuf;

/// Parse armyknife-llm-redteam JSON output into securegit Findings.
///
/// armyknife returns findings as JSON arrays with fields:
/// probe_name, title, description, severity, category, remediation, evidence
pub fn parse_redteam_findings(json_text: &str) -> Vec<Finding> {
    let value: serde_json::Value = match serde_json::from_str(json_text) {
        Ok(v) => v,
        Err(_) => return Vec::new(),
    };

    // Handle both { "findings": [...] } and direct [...]
    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().filter_map(parse_single_finding).collect()
}

fn parse_single_finding(value: &serde_json::Value) -> Option<Finding> {
    let probe_name = value.get("probe_name")?.as_str()?;
    let title = value
        .get("title")
        .and_then(|v| v.as_str())
        .unwrap_or(probe_name);
    let severity_str = value
        .get("severity")
        .and_then(|v| v.as_str())
        .unwrap_or("medium");

    let severity = Severity::parse_str(severity_str).unwrap_or(Severity::Medium);

    let mut finding = Finding::new(
        format!("redteam-{}", probe_name),
        title.to_string(),
        severity,
    );

    finding.confidence = Confidence::Medium;

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

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

    // Map category to tags
    if let Some(category) = value.get("category").and_then(|v| v.as_str()) {
        finding.tags.push("llm-security".to_string());
        finding.tags.push(category.to_lowercase());
    }

    // Evidence from input/output
    if let Some(input) = value.get("input").and_then(|v| v.as_str()) {
        finding.evidence.push(format!("Input: {}", input));
    }
    if let Some(output) = value.get("output").and_then(|v| v.as_str()) {
        finding.evidence.push(format!("Output: {}", output));
    }

    // Tool name as file_path for MCP scan results
    if let Some(tool) = value.get("tool_name").and_then(|v| v.as_str()) {
        finding.file_path = Some(PathBuf::from(tool));
    }

    // CWE from probe metadata
    if let Some(cwe) = value.get("cwe").and_then(|v| v.as_u64()) {
        finding.cwe_ids.push(cwe as u32);
    }

    Some(finding)
}

/// Parse MCP scan results specifically (scan_mcp tool output).
pub fn parse_mcp_scan_findings(json_text: &str, config_path: Option<&str>) -> Vec<Finding> {
    let mut findings = parse_redteam_findings(json_text);

    // Set file_path to the MCP config file that was scanned
    if let Some(path) = config_path {
        for f in &mut findings {
            if f.file_path.is_none() {
                f.file_path = Some(PathBuf::from(path));
            }
        }
    }

    findings
}