agentshield/output/
json.rs1use 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#[derive(Serialize)]
12struct FindingWithFingerprint<'a> {
13 #[serde(flatten)]
14 finding: &'a Finding,
15 fingerprint: String,
16}
17
18use crate::rules::Severity;
19
20#[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
66pub 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}