use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonSourceLocation {
pub file: String,
pub line: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub function: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonSyscall {
pub name: String,
pub args: Vec<String>,
pub result: i64,
#[serde(skip_serializing_if = "Option::is_none")]
pub duration_us: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub source: Option<JsonSourceLocation>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonMlAnalysis {
pub clusters: usize,
pub silhouette_score: f64,
pub anomalies: Vec<JsonMlAnomaly>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonMlAnomaly {
pub syscall: String,
pub avg_time_us: f64,
pub cluster: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonIsolationForestAnalysis {
pub num_trees: usize,
pub contamination: f32,
pub total_samples: usize,
pub outliers: Vec<JsonIsolationForestOutlier>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonIsolationForestOutlier {
pub syscall: String,
pub anomaly_score: f64,
pub avg_duration_us: f64,
pub call_count: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub feature_importance: Option<Vec<JsonFeatureImportance>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonFeatureImportance {
pub feature: String,
pub importance: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonAutoencoderAnalysis {
pub hidden_size: usize,
pub epochs: usize,
pub threshold: f32,
pub adaptive_threshold: f64,
pub total_samples: usize,
pub anomalies: Vec<JsonAutoencoderAnomaly>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonAutoencoderAnomaly {
pub syscall: String,
pub reconstruction_error: f64,
pub avg_duration_us: f64,
pub call_count: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub feature_contributions: Option<Vec<JsonFeatureImportance>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonSummary {
pub total_syscalls: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub total_time_us: Option<u64>,
pub exit_code: i32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonOutput {
pub version: String,
pub format: String,
pub syscalls: Vec<JsonSyscall>,
pub summary: JsonSummary,
#[serde(skip_serializing_if = "Option::is_none")]
pub ml_analysis: Option<JsonMlAnalysis>,
#[serde(skip_serializing_if = "Option::is_none")]
pub isolation_forest_analysis: Option<JsonIsolationForestAnalysis>,
#[serde(skip_serializing_if = "Option::is_none")]
pub autoencoder_analysis: Option<JsonAutoencoderAnalysis>,
}
impl JsonOutput {
pub fn new() -> Self {
Self {
version: env!("CARGO_PKG_VERSION").to_string(),
format: "renacer-json-v1".to_string(),
syscalls: Vec::new(),
summary: JsonSummary { total_syscalls: 0, total_time_us: None, exit_code: 0 },
ml_analysis: None,
isolation_forest_analysis: None,
autoencoder_analysis: None,
}
}
pub fn add_syscall(&mut self, syscall: JsonSyscall) {
self.summary.total_syscalls += 1;
if let Some(duration) = syscall.duration_us {
*self.summary.total_time_us.get_or_insert(0) += duration;
}
self.syscalls.push(syscall);
}
pub fn set_exit_code(&mut self, code: i32) {
self.summary.exit_code = code;
}
pub fn set_ml_analysis(&mut self, report: crate::ml_anomaly::MlAnomalyReport) {
let anomalies = report
.anomalies
.iter()
.map(|a| JsonMlAnomaly {
syscall: a.syscall.clone(),
avg_time_us: a.avg_time_us,
cluster: a.cluster,
})
.collect();
self.ml_analysis = Some(JsonMlAnalysis {
clusters: report.num_clusters,
silhouette_score: report.silhouette_score,
anomalies,
});
}
pub fn set_isolation_forest_analysis(
&mut self,
report: crate::isolation_forest::OutlierReport,
explain: bool,
) {
let outliers = report
.outliers
.iter()
.map(|o| {
let feature_importance = if explain && !o.feature_importance.is_empty() {
Some(
o.feature_importance
.iter()
.map(|(feature, importance)| JsonFeatureImportance {
feature: feature.clone(),
importance: *importance,
})
.collect(),
)
} else {
None
};
JsonIsolationForestOutlier {
syscall: o.syscall.clone(),
anomaly_score: o.anomaly_score,
avg_duration_us: o.avg_duration_us,
call_count: o.call_count,
feature_importance,
}
})
.collect();
self.isolation_forest_analysis = Some(JsonIsolationForestAnalysis {
num_trees: report.num_trees,
contamination: report.contamination,
total_samples: report.total_samples,
outliers,
});
}
pub fn set_autoencoder_analysis(
&mut self,
report: crate::autoencoder::AutoencoderReport,
threshold: f32,
explain: bool,
) {
let anomalies = report
.anomalies
.iter()
.map(|a| {
let feature_contributions = if explain && !a.feature_contributions.is_empty() {
Some(
a.feature_contributions
.iter()
.map(|(feature, contribution)| JsonFeatureImportance {
feature: feature.clone(),
importance: *contribution,
})
.collect(),
)
} else {
None
};
JsonAutoencoderAnomaly {
syscall: a.syscall.clone(),
reconstruction_error: a.reconstruction_error,
avg_duration_us: a.avg_duration_us,
call_count: a.call_count,
feature_contributions,
}
})
.collect();
self.autoencoder_analysis = Some(JsonAutoencoderAnalysis {
hidden_size: report.hidden_size,
epochs: report.epochs,
threshold,
adaptive_threshold: report.threshold,
total_samples: report.total_samples,
anomalies,
});
}
pub fn to_json(&self) -> anyhow::Result<String> {
Ok(serde_json::to_string_pretty(self)?)
}
}
impl Default for JsonOutput {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_json_output_creation() {
let output = JsonOutput::new();
assert_eq!(output.format, "renacer-json-v1");
assert_eq!(output.syscalls.len(), 0);
assert_eq!(output.summary.total_syscalls, 0);
}
#[test]
fn test_json_output_default() {
let output = JsonOutput::default();
assert_eq!(output.format, "renacer-json-v1");
assert!(output.ml_analysis.is_none());
assert!(output.isolation_forest_analysis.is_none());
assert!(output.autoencoder_analysis.is_none());
}
#[test]
fn test_add_syscall() {
let mut output = JsonOutput::new();
let syscall = JsonSyscall {
name: "write".to_string(),
args: vec!["1".to_string(), "\"hello\"".to_string(), "5".to_string()],
result: 5,
duration_us: Some(100),
source: None,
};
output.add_syscall(syscall);
assert_eq!(output.summary.total_syscalls, 1);
assert_eq!(output.summary.total_time_us, Some(100));
}
#[test]
fn test_add_multiple_syscalls_accumulates_time() {
let mut output = JsonOutput::new();
output.add_syscall(JsonSyscall {
name: "read".to_string(),
args: vec![],
result: 10,
duration_us: Some(50),
source: None,
});
output.add_syscall(JsonSyscall {
name: "write".to_string(),
args: vec![],
result: 10,
duration_us: Some(75),
source: None,
});
assert_eq!(output.summary.total_syscalls, 2);
assert_eq!(output.summary.total_time_us, Some(125)); }
#[test]
fn test_add_syscall_without_duration() {
let mut output = JsonOutput::new();
output.add_syscall(JsonSyscall {
name: "read".to_string(),
args: vec![],
result: 10,
duration_us: None,
source: None,
});
assert_eq!(output.summary.total_syscalls, 1);
assert!(output.summary.total_time_us.is_none());
}
#[test]
fn test_set_exit_code() {
let mut output = JsonOutput::new();
output.set_exit_code(42);
assert_eq!(output.summary.exit_code, 42);
}
#[test]
fn test_set_ml_analysis() {
use crate::ml_anomaly::{MlAnomaly, MlAnomalyReport};
use std::collections::HashMap;
let mut output = JsonOutput::new();
let report = MlAnomalyReport {
num_clusters: 3,
silhouette_score: 0.75,
cluster_assignments: HashMap::new(),
cluster_centers: vec![100.0, 200.0, 500.0],
total_samples: 100,
anomalies: vec![MlAnomaly {
syscall: "poll".to_string(),
avg_time_us: 150.0,
cluster: 2,
distance: 50.0,
}],
};
output.set_ml_analysis(report);
let analysis = output.ml_analysis.expect("ML analysis should be set");
assert_eq!(analysis.clusters, 3);
assert!((analysis.silhouette_score - 0.75).abs() < f64::EPSILON);
assert_eq!(analysis.anomalies.len(), 1);
assert_eq!(analysis.anomalies[0].syscall, "poll");
}
#[test]
fn test_set_isolation_forest_analysis_without_explain() {
use crate::isolation_forest::{Outlier, OutlierReport};
let mut output = JsonOutput::new();
let report = OutlierReport {
num_trees: 100,
contamination: 0.05,
total_samples: 500,
outliers: vec![Outlier {
syscall: "epoll_wait".to_string(),
anomaly_score: 0.85,
avg_duration_us: 5000.0,
call_count: 10,
feature_importance: vec![],
}],
};
output.set_isolation_forest_analysis(report, false);
let analysis =
output.isolation_forest_analysis.expect("Isolation forest analysis should be set");
assert_eq!(analysis.num_trees, 100);
assert!((analysis.contamination - 0.05).abs() < f32::EPSILON);
assert_eq!(analysis.total_samples, 500);
assert_eq!(analysis.outliers.len(), 1);
assert!(analysis.outliers[0].feature_importance.is_none());
}
#[test]
fn test_set_isolation_forest_analysis_with_explain() {
use crate::isolation_forest::{Outlier, OutlierReport};
let mut output = JsonOutput::new();
let report = OutlierReport {
num_trees: 50,
contamination: 0.1,
total_samples: 200,
outliers: vec![Outlier {
syscall: "futex".to_string(),
anomaly_score: 0.92,
avg_duration_us: 10000.0,
call_count: 5,
feature_importance: vec![
("duration".to_string(), 45.0),
("frequency".to_string(), 35.0),
],
}],
};
output.set_isolation_forest_analysis(report, true);
let analysis = output.isolation_forest_analysis.expect("Analysis set");
let outlier = &analysis.outliers[0];
let importance = outlier.feature_importance.as_ref().expect("Features set");
assert_eq!(importance.len(), 2);
assert_eq!(importance[0].feature, "duration");
assert!((importance[0].importance - 45.0).abs() < f64::EPSILON);
}
#[test]
fn test_set_autoencoder_analysis_without_explain() {
use crate::autoencoder::{Anomaly, AutoencoderReport};
let mut output = JsonOutput::new();
let report = AutoencoderReport {
hidden_size: 32,
epochs: 100,
threshold: 0.05,
total_samples: 1000,
anomalies: vec![Anomaly {
syscall: "sendmsg".to_string(),
reconstruction_error: 0.12,
avg_duration_us: 250.0,
call_count: 50,
feature_contributions: vec![],
}],
};
output.set_autoencoder_analysis(report, 2.0, false);
let analysis = output.autoencoder_analysis.expect("Autoencoder analysis should be set");
assert_eq!(analysis.hidden_size, 32);
assert_eq!(analysis.epochs, 100);
assert!((analysis.threshold - 2.0).abs() < f32::EPSILON);
assert_eq!(analysis.anomalies.len(), 1);
assert!(analysis.anomalies[0].feature_contributions.is_none());
}
#[test]
fn test_set_autoencoder_analysis_with_explain() {
use crate::autoencoder::{Anomaly, AutoencoderReport};
let mut output = JsonOutput::new();
let report = AutoencoderReport {
hidden_size: 64,
epochs: 200,
threshold: 0.08,
total_samples: 2000,
anomalies: vec![Anomaly {
syscall: "recvfrom".to_string(),
reconstruction_error: 0.25,
avg_duration_us: 800.0,
call_count: 100,
feature_contributions: vec![
("latency".to_string(), 60.0),
("variance".to_string(), 40.0),
],
}],
};
output.set_autoencoder_analysis(report, 3.0, true);
let analysis = output.autoencoder_analysis.expect("Analysis set");
let anomaly = &analysis.anomalies[0];
let contributions = anomaly.feature_contributions.as_ref().expect("Features set");
assert_eq!(contributions.len(), 2);
assert_eq!(contributions[0].feature, "latency");
}
#[test]
fn test_json_serialization() {
let mut output = JsonOutput::new();
output.add_syscall(JsonSyscall {
name: "openat".to_string(),
args: vec!["0xffffff9c".to_string(), "\"/tmp/test\"".to_string(), "0x2".to_string()],
result: 3,
duration_us: None,
source: Some(JsonSourceLocation {
file: "main.rs".to_string(),
line: 42,
function: Some("main".to_string()),
}),
});
output.set_exit_code(0);
let json = output.to_json().expect("test");
assert!(json.contains("\"name\": \"openat\""));
assert!(json.contains("\"format\": \"renacer-json-v1\""));
assert!(json.contains("\"file\": \"main.rs\""));
assert!(json.contains("\"line\": 42"));
}
#[test]
fn test_optional_fields_omitted() {
let syscall = JsonSyscall {
name: "read".to_string(),
args: vec!["3".to_string()],
result: 10,
duration_us: None,
source: None,
};
let json = serde_json::to_string(&syscall).expect("test");
assert!(!json.contains("duration_us"));
assert!(!json.contains("source"));
}
#[test]
fn test_source_location_without_function() {
let loc = JsonSourceLocation { file: "test.rs".to_string(), line: 10, function: None };
let json = serde_json::to_string(&loc).expect("test");
assert!(json.contains("\"file\":\"test.rs\"") || json.contains("\"file\": \"test.rs\""));
assert!(!json.contains("function"));
}
#[test]
fn test_json_deserialization() {
let json = r#"{
"version": "0.7.0",
"format": "renacer-json-v1",
"syscalls": [{
"name": "write",
"args": ["1", "hello"],
"result": 5
}],
"summary": {
"total_syscalls": 1,
"exit_code": 0
}
}"#;
let output: JsonOutput = serde_json::from_str(json).expect("test");
assert_eq!(output.syscalls.len(), 1);
assert_eq!(output.syscalls[0].name, "write");
assert_eq!(output.summary.total_syscalls, 1);
}
}