openclaw-scan 0.1.1

Security scanner for agentic AI framework installations (OpenClaw, Claude Code, and compatible)
Documentation
//! Machine-readable JSON output.
//!
//! Writes the full [`Report`] as pretty-printed JSON to stdout.
//! The schema is stable and suitable for piping to `jq`, CI tools, and dashboards.

use crate::report::Report;

/// Print `report` as pretty-printed JSON to stdout.
pub fn print(report: &Report) -> anyhow::Result<()> {
    let stdout = std::io::stdout();
    serde_json::to_writer_pretty(stdout.lock(), report)?;
    // Ensure the output ends with a newline for shell-friendliness.
    println!();
    Ok(())
}

// ── Tests ─────────────────────────────────────────────────────────────────────

#[cfg(test)]
mod tests {
    use crate::finding::{Category, Finding, Severity};
    use crate::report::Report;

    fn make_report() -> Report {
        let findings = vec![
            Finding::new(
                Severity::High,
                Category::SecretDetection,
                "Test finding",
                "A test finding description.",
                "/tmp/test.json",
                "Fix it.",
            ),
            Finding::new(
                Severity::Info,
                Category::DataExposure,
                "Info finding",
                "Informational.",
                "/tmp/test2.json",
                "No action needed.",
            ),
        ];
        Report::build(findings, vec!["/tmp".to_string()], "0.1.0-test")
    }

    #[test]
    fn report_serialises_to_valid_json() {
        let report = make_report();
        let json_str = serde_json::to_string_pretty(&report).expect("serialization failed");

        // Must include top-level fields.
        assert!(json_str.contains("\"version\""));
        assert!(json_str.contains("\"overall_score\""));
        assert!(json_str.contains("\"overall_grade\""));
        assert!(json_str.contains("\"findings\""));
        assert!(json_str.contains("\"categories\""));
        assert!(json_str.contains("\"scanned_at\""));
    }

    #[test]
    fn report_json_contains_severity_labels() {
        let report = make_report();
        let json_str = serde_json::to_string(&report).expect("serialization failed");
        // Severity serialises as lowercase (serde rename_all = "lowercase")
        assert!(json_str.contains("\"high\""));
        assert!(json_str.contains("\"info\""));
    }

    #[test]
    fn report_json_severity_sorted_most_severe_first() {
        let report = make_report();
        let json_str = serde_json::to_string_pretty(&report).expect("serialization failed");
        let high_pos = json_str.find("\"high\"").unwrap();
        let info_pos = json_str.find("\"info\"").unwrap();
        assert!(
            high_pos < info_pos,
            "HIGH should appear before INFO in sorted findings"
        );
    }
}