Skip to main content

agentshield/output/
json.rs

1use std::path::Path;
2
3use chrono::Utc;
4use serde::Serialize;
5
6use crate::error::Result;
7use crate::rules::Finding;
8use crate::rules::policy::PolicyVerdict;
9
10/// A finding entry with an attached fingerprint for JSON output.
11#[derive(Serialize)]
12struct FindingWithFingerprint<'a> {
13    #[serde(flatten)]
14    finding: &'a Finding,
15    fingerprint: String,
16}
17
18use crate::rules::Severity;
19
20/// Summary counts of findings by severity.
21#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
22pub struct JsonSummary {
23    pub total: usize,
24    pub critical: usize,
25    pub high: usize,
26    pub medium: usize,
27    pub low: usize,
28    pub info: usize,
29}
30
31impl JsonSummary {
32    pub fn from_findings(findings: &[Finding]) -> Self {
33        let mut summary = Self {
34            total: findings.len(),
35            critical: 0,
36            high: 0,
37            medium: 0,
38            low: 0,
39            info: 0,
40        };
41        for f in findings {
42            match f.severity {
43                Severity::Critical => summary.critical += 1,
44                Severity::High => summary.high += 1,
45                Severity::Medium => summary.medium += 1,
46                Severity::Low => summary.low += 1,
47                Severity::Info => summary.info += 1,
48            }
49        }
50        summary
51    }
52}
53
54#[derive(Serialize)]
55struct JsonReport<'a> {
56    schema_version: &'static str,
57    tool_version: &'static str,
58    target: &'a str,
59    scan_root: String,
60    generated_at: String,
61    summary: JsonSummary,
62    verdict: &'a PolicyVerdict,
63    findings: Vec<FindingWithFingerprint<'a>>,
64}
65
66/// Render findings as a JSON report, with a `fingerprint` and scan metadata.
67pub fn render(
68    findings: &[Finding],
69    verdict: &PolicyVerdict,
70    target_name: &str,
71    scan_root: &Path,
72) -> Result<String> {
73    let findings_with_fp: Vec<FindingWithFingerprint<'_>> = findings
74        .iter()
75        .map(|f| FindingWithFingerprint {
76            finding: f,
77            fingerprint: f.fingerprint(scan_root),
78        })
79        .collect();
80
81    let summary = JsonSummary::from_findings(findings);
82
83    let report = JsonReport {
84        schema_version: "1.0.0",
85        tool_version: env!("CARGO_PKG_VERSION"),
86        target: target_name,
87        scan_root: scan_root.to_string_lossy().into_owned(),
88        generated_at: Utc::now().to_rfc3339(),
89        summary,
90        verdict,
91        findings: findings_with_fp,
92    };
93
94    let json = serde_json::to_string_pretty(&report)?;
95    Ok(json)
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101    use crate::rules::{AttackCategory, Confidence};
102    use std::path::PathBuf;
103
104    #[test]
105    fn test_json_render_includes_metadata_and_summary() {
106        let finding = Finding {
107            rule_id: "SHIELD-001".into(),
108            rule_name: "Command Injection".into(),
109            severity: Severity::Critical,
110            confidence: Confidence::High,
111            attack_category: AttackCategory::CommandInjection,
112            message: "eval input".into(),
113            location: None,
114            evidence: vec![],
115            taint_path: None,
116            remediation: None,
117            cwe_id: Some("CWE-78".into()),
118        };
119
120        let verdict = PolicyVerdict {
121            pass: false,
122            total_findings: 1,
123            effective_findings: 1,
124            highest_severity: Some(Severity::Critical),
125            fail_threshold: Severity::High,
126        };
127
128        let output = render(&[finding], &verdict, "test_target", &PathBuf::from("/test")).unwrap();
129        let value: serde_json::Value = serde_json::from_str(&output).unwrap();
130
131        assert_eq!(value["schema_version"], "1.0.0");
132        assert_eq!(value["target"], "test_target");
133        assert_eq!(value["summary"]["total"], 1);
134        assert_eq!(value["summary"]["critical"], 1);
135        assert_eq!(value["summary"]["high"], 0);
136        assert_eq!(value["findings"].as_array().unwrap().len(), 1);
137        assert!(value["findings"][0]["fingerprint"].is_string());
138    }
139}