zift 0.1.6

Scan codebases for embedded authorization logic and generate Policy as Code (Rego/OPA today)
Documentation
use std::collections::HashMap;
use std::io::Write;
use std::path::Path;

use serde::Serialize;

use crate::error::Result;
use crate::types::Finding;

#[derive(Serialize)]
struct ScanReport<'a> {
    version: &'static str,
    scan_root: &'a Path,
    findings: &'a [Finding],
    summary: Summary,
}

#[derive(Serialize)]
struct Summary {
    total_findings: usize,
    enforcement_points: usize,
    externalized_pct: usize,
    by_category: HashMap<String, usize>,
    by_confidence: HashMap<String, usize>,
    files_with_findings: usize,
}

pub fn print(
    findings: &[Finding],
    scan_root: &Path,
    enforcement_points: usize,
    writer: &mut dyn Write,
) -> Result<()> {
    let mut by_category: HashMap<String, usize> = HashMap::new();
    let mut by_confidence: HashMap<String, usize> = HashMap::new();
    let mut files = std::collections::HashSet::new();

    for f in findings {
        // Use the canonical snake_case wire form so summary keys round-trip
        // against `findings[].category` in the same document. The Display impl
        // produces a human-friendly form (`"Business Rule"` → lowercased
        // `"business rule"` with a space), which disagrees with the serde
        // form (`"business_rule"`) on multi-word variants and breaks
        // consumers grouping the summary by category.
        *by_category
            .entry(f.category.slug().to_string())
            .or_default() += 1;
        *by_confidence.entry(f.confidence.to_string()).or_default() += 1;
        files.insert(&f.file);
    }

    let report = ScanReport {
        version: env!("CARGO_PKG_VERSION"),
        scan_root,
        findings,
        summary: Summary {
            total_findings: findings.len(),
            enforcement_points,
            externalized_pct: if findings.is_empty() && enforcement_points == 0 {
                0
            } else {
                let total = findings.len() + enforcement_points;
                (enforcement_points as f64 / total as f64 * 100.0).round() as usize
            },
            by_category,
            by_confidence,
            files_with_findings: files.len(),
        },
    };

    serde_json::to_writer_pretty(writer, &report)?;
    Ok(())
}

impl From<serde_json::Error> for crate::error::ZiftError {
    fn from(e: serde_json::Error) -> Self {
        crate::error::ZiftError::General(format!("JSON serialization error: {e}"))
    }
}