mycop 0.3.1

AI Code Security Scanner — detect vulnerabilities in AI-generated code
Documentation
use serde_json::json;
use std::collections::HashMap;

use crate::reporter::Reporter;
use crate::rules::matcher::Finding;

pub struct JsonReporter;

impl Default for JsonReporter {
    fn default() -> Self {
        Self
    }
}

impl JsonReporter {
    pub fn new() -> Self {
        Self
    }
}

impl Reporter for JsonReporter {
    fn report(
        &self,
        findings: &[Finding],
        ai_results: &HashMap<usize, String>,
    ) -> anyhow::Result<String> {
        let findings_json: Vec<serde_json::Value> = findings
            .iter()
            .enumerate()
            .map(|(idx, f)| {
                let mut obj = json!({
                    "ruleId": f.rule_id,
                    "ruleName": f.rule_name,
                    "severity": f.severity.label(),
                    "file": f.file.display().to_string(),
                    "line": f.line,
                    "column": f.column,
                    "matchedText": f.matched_text,
                    "message": f.message,
                    "description": f.description,
                });

                if let Some(ref cwe) = f.cwe {
                    obj["cwe"] = json!(cwe);
                }
                if let Some(ref owasp) = f.owasp {
                    obj["owasp"] = json!(owasp);
                }
                if let Some(ref hint) = f.fix_hint {
                    obj["fixHint"] = json!(hint);
                }
                if !f.references.is_empty() {
                    obj["references"] = json!(f.references);
                }
                if let Some(ai_text) = ai_results.get(&idx) {
                    obj["aiExplanation"] = json!(ai_text);
                }

                obj
            })
            .collect();

        let output = json!({
            "version": env!("CARGO_PKG_VERSION"),
            "totalFindings": findings.len(),
            "findings": findings_json,
        });

        let json_str = serde_json::to_string_pretty(&output)?;
        println!("{}", json_str);
        Ok(json_str)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::rules::parser::Severity;
    use std::path::PathBuf;

    fn make_finding(rule_id: &str, severity: Severity) -> Finding {
        Finding {
            rule_id: rule_id.to_string(),
            rule_name: "test-rule".to_string(),
            severity,
            file: PathBuf::from("test.py"),
            line: 10,
            column: 1,
            matched_text: "eval(input)".to_string(),
            context_before: vec![],
            context_after: vec![],
            message: "Dangerous eval".to_string(),
            fix_hint: Some("Use ast.literal_eval".to_string()),
            cwe: Some("CWE-95".to_string()),
            owasp: None,
            description: "Eval injection".to_string(),
            references: vec![],
        }
    }

    #[test]
    fn test_json_output_structure() {
        let findings = vec![make_finding("PY-SEC-005", Severity::High)];
        let ai_results = HashMap::new();
        let reporter = JsonReporter::new();
        let result = reporter.report(&findings, &ai_results).unwrap();
        let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();

        assert_eq!(parsed["totalFindings"], 1);
        assert!(parsed["version"].is_string());
        assert!(parsed["findings"].is_array());
        assert_eq!(parsed["findings"][0]["ruleId"], "PY-SEC-005");
        assert_eq!(parsed["findings"][0]["severity"], "HIGH");
        assert_eq!(parsed["findings"][0]["cwe"], "CWE-95");
    }

    #[test]
    fn test_json_empty_findings() {
        let findings = vec![];
        let ai_results = HashMap::new();
        let reporter = JsonReporter::new();
        let result = reporter.report(&findings, &ai_results).unwrap();
        let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();

        assert_eq!(parsed["totalFindings"], 0);
        assert_eq!(parsed["findings"].as_array().unwrap().len(), 0);
    }
}