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::cli::UI;
use crate::core::{Config, ScanEngine, Severity};
use crate::platform;
use crate::platform::types::CreateIssue;
use anyhow::Result;
use std::collections::HashSet;
use std::path::PathBuf;

pub async fn execute(
    path: PathBuf,
    fail_on: Option<String>,
    include_git: bool,
    create_issues: bool,
    issue_threshold: String,
    ui: &UI,
) -> Result<()> {
    ui.header("Security Scan");
    ui.blank();
    ui.field("Target", path.display());
    ui.field("Include .git", include_git);
    ui.blank();

    // Load configuration
    let config = Config::default();
    let engine = ScanEngine::new(config);

    // Scan the directory
    let mut report = engine.scan_directory(&path).await?;

    // Optionally scan .git directory
    if include_git {
        let git_dir = path.join(".git");
        if git_dir.exists() {
            let git_report = engine.scan_git_directory(&git_dir).await?;
            report.merge(git_report);
        }
    }

    // Display results
    if ui.json {
        let json_value = serde_json::to_value(&report).unwrap_or_default();
        ui.json_out(&json_value);
    } else {
        display_report(&report, ui);
    }

    // File issues if requested
    if create_issues && !report.findings.is_empty() {
        let issue_sev = Severity::parse_str(&issue_threshold).unwrap_or(Severity::High);
        let qualifying: Vec<_> = report
            .findings
            .iter()
            .filter(|f| f.severity >= issue_sev)
            .collect();

        if !qualifying.is_empty() {
            ui.blank();
            ui.section("Filing Issues");

            match file_issues_from_findings(&qualifying, ui).await {
                Ok((created, skipped)) => {
                    ui.blank();
                    ui.success(format!("{} issues created, {} skipped", created, skipped));
                }
                Err(e) => {
                    ui.blank();
                    ui.warning(format!("Could not file issues: {}", e));
                }
            }
        }
    }

    // Determine exit code based on fail_on threshold
    let threshold = match fail_on.as_deref() {
        Some("critical") => Severity::Critical,
        Some("high") => Severity::High,
        Some("medium") => Severity::Medium,
        Some("low") => Severity::Low,
        _ => Severity::High, // Default to high
    };

    if report.has_findings_at_or_above(threshold) {
        anyhow::bail!(
            "Scan failed: Found {} findings at or above {} severity",
            report
                .findings
                .iter()
                .filter(|f| f.severity >= threshold)
                .count(),
            threshold
        );
    }

    Ok(())
}

async fn file_issues_from_findings(
    findings: &[&crate::core::Finding],
    ui: &UI,
) -> Result<(usize, usize)> {
    let path = PathBuf::from(".");
    let remote = platform::detect_remote(&path)?;
    let token = platform::resolve_token(&remote.host)
        .ok_or_else(|| anyhow::anyhow!("Not authenticated. Run: securegit auth login"))?;
    let client = platform::create_client(&remote, token);

    // Deduplicate by title+file
    let mut seen = HashSet::new();
    let mut created = 0usize;
    let mut skipped = 0usize;

    for finding in findings {
        let file_str = finding
            .file_path
            .as_ref()
            .map(|p| p.display().to_string())
            .unwrap_or_else(|| "unknown".to_string());

        let dedup_key = format!("{}:{}", finding.title, file_str);
        if !seen.insert(dedup_key) {
            skipped += 1;
            continue;
        }

        let issue_title = format!(
            "[SecureGit] {}: {} in {}",
            finding.severity, finding.title, file_str
        );

        // Check if issue already exists
        let existing = client
            .search_issues(&format!("[SecureGit] {} in {}", finding.title, file_str))
            .await
            .unwrap_or_default();

        if existing
            .iter()
            .any(|i| i.title.contains(&finding.title) && i.state == "open")
        {
            ui.status_item(false, format!("Exists: {}", issue_title));
            skipped += 1;
            continue;
        }

        // Build issue body
        let mut body = "## Security Finding\n\n".to_string();
        body.push_str(&format!("**Severity:** {}\n", finding.severity));
        body.push_str(&format!("**File:** `{}`\n", file_str));
        if let Some(line) = finding.line_start {
            body.push_str(&format!("**Line:** {}\n", line));
        }
        body.push_str(&format!("\n### Description\n\n{}\n", finding.description));

        if let Some(ref snippet) = finding.code_snippet {
            body.push_str(&format!("\n### Evidence\n\n```\n{}\n```\n", snippet));
        }

        if let Some(ref remediation) = finding.remediation {
            body.push_str(&format!("\n### Remediation\n\n{}\n", remediation));
        }

        if !finding.cwe_ids.is_empty() {
            body.push_str("\n### References\n\n");
            for cwe in &finding.cwe_ids {
                body.push_str(&format!(
                    "- [CWE-{}](https://cwe.mitre.org/data/definitions/{}.html)\n",
                    cwe, cwe
                ));
            }
        }

        body.push_str("\n---\n*Filed automatically by securegit*\n");

        let severity_label = format!("{}", finding.severity).to_lowercase();
        let labels = vec!["security".to_string(), severity_label];

        match client
            .create_issue(&CreateIssue {
                title: issue_title.clone(),
                body,
                labels,
            })
            .await
        {
            Ok(issue) => {
                ui.status_item(true, format!("Created #{}: {}", issue.number, issue_title));
                created += 1;
            }
            Err(e) => {
                ui.status_item(false, format!("Failed: {} ({})", issue_title, e));
                skipped += 1;
            }
        }
    }

    Ok((created, skipped))
}

fn display_report(report: &crate::core::ScanReport, ui: &UI) {
    ui.section("Scan Results");

    // Summary statistics
    ui.field("Files scanned", report.scanned_files);
    ui.field(
        "Data scanned",
        format!("{} KB", report.scanned_bytes / 1024),
    );
    ui.field("Duration", format!("{} ms", report.duration_ms));
    ui.blank();

    // Findings by severity
    let critical = report.count_by_severity(Severity::Critical);
    let high = report.count_by_severity(Severity::High);
    let medium = report.count_by_severity(Severity::Medium);
    let low = report.count_by_severity(Severity::Low);
    let info_count = report.count_by_severity(Severity::Info);

    ui.severity_row(critical, high, medium, low, info_count);

    if report.findings.is_empty() {
        ui.result_banner(true, "No security issues found", &[]);
        return;
    }

    ui.blank();

    // Group findings by severity and display
    for severity in [
        Severity::Critical,
        Severity::High,
        Severity::Medium,
        Severity::Low,
    ] {
        let findings: Vec<_> = report
            .findings
            .iter()
            .filter(|f| f.severity == severity)
            .collect();

        if findings.is_empty() {
            continue;
        }

        for finding in findings {
            ui.finding(finding);
        }
    }

    // Plugin performance summary
    if !report.plugin_reports.is_empty() {
        ui.section("Plugin Performance:");
        for plugin_report in &report.plugin_reports {
            if plugin_report.findings_count > 0 {
                ui.list_item(format!(
                    "{} - {} findings in {}ms",
                    plugin_report.plugin_name,
                    plugin_report.findings_count,
                    plugin_report.duration_ms
                ));
            }
        }
        ui.blank();
    }
}