use std::io::Write;
use schemars::JsonSchema;
use serde::Serialize;
use whyno_core::checks::{CheckReport, LayerResult};
use whyno_core::fix::{commands, FixPlan};
use whyno_core::state::SystemState;
use super::layer_name;
use crate::error::OutputError;
pub fn render(
report: &CheckReport,
plan: &FixPlan,
state: &SystemState,
writer: &mut dyn Write,
) -> Result<(), OutputError> {
let output = build_output(report, plan, state);
serde_json::to_writer_pretty(&mut *writer, &output)?;
writeln!(writer)?;
Ok(())
}
fn build_output(report: &CheckReport, plan: &FixPlan, state: &SystemState) -> JsonOutput {
let target = state
.walk
.last()
.map(|c| c.path.display().to_string())
.unwrap_or_default();
JsonOutput {
version: 1,
subject: build_subject(state),
operation: layer_name::operation_name(state.operation),
target,
result: overall_result(report),
layers: build_layers(report),
fixes: build_fixes(plan),
warnings: plan.warnings.clone(),
degraded: build_degraded(report),
}
}
fn build_subject(state: &SystemState) -> JsonSubject {
JsonSubject {
uid: state.subject.uid,
gid: state.subject.gid,
groups: state.subject.groups.clone(),
}
}
fn overall_result(report: &CheckReport) -> String {
if report.is_allowed() {
let any_degraded = report.core_results.values().any(LayerResult::is_degraded);
if any_degraded { "degraded" } else { "allowed" }.to_string()
} else {
"denied".to_string()
}
}
fn build_layers(report: &CheckReport) -> Vec<JsonLayer> {
let mut out: Vec<JsonLayer> = report
.core_results
.iter()
.map(|(l, r)| layer_to_json(layer_name::short(l).to_string(), r))
.collect();
for (l, r) in &report.mac_results {
out.push(layer_to_json(layer_name::short_mac(*l).to_string(), r));
}
out
}
fn layer_to_json(name: String, result: &LayerResult) -> JsonLayer {
match result {
LayerResult::Pass { detail, warnings } => JsonLayer {
name,
result: "pass".to_string(),
detail: detail.clone(),
warnings: warnings.clone(),
},
LayerResult::Fail { detail, .. } => JsonLayer {
name,
result: "fail".to_string(),
detail: detail.clone(),
warnings: vec![],
},
LayerResult::Degraded { reason } => JsonLayer {
name,
result: "degraded".to_string(),
detail: reason.clone(),
warnings: vec![],
},
_ => JsonLayer {
name,
result: "unknown".to_string(),
detail: String::new(),
warnings: vec![],
},
}
}
fn build_fixes(plan: &FixPlan) -> Vec<JsonFix> {
plan.fixes
.iter()
.map(|f| JsonFix {
command: commands::render_command(&f.action),
impact: f.impact,
layer: layer_name::short(f.layer).to_string(),
description: f.description.clone(),
})
.collect()
}
fn build_degraded(report: &CheckReport) -> Vec<JsonDegraded> {
report
.core_results
.iter()
.filter_map(|(l, r)| match r {
LayerResult::Degraded { reason } => Some(JsonDegraded {
layer: layer_name::short(l).to_string(),
reason: reason.clone(),
}),
_ => None,
})
.collect()
}
pub fn generate_schema() -> schemars::Schema {
schemars::schema_for!(JsonOutput)
}
#[derive(Debug, Serialize, JsonSchema)]
pub(crate) struct JsonOutput {
version: u32,
subject: JsonSubject,
operation: String,
target: String,
result: String,
layers: Vec<JsonLayer>,
fixes: Vec<JsonFix>,
warnings: Vec<String>,
degraded: Vec<JsonDegraded>,
}
#[derive(Debug, Serialize, JsonSchema)]
struct JsonSubject {
uid: u32,
gid: u32,
groups: Vec<u32>,
}
#[derive(Debug, Serialize, JsonSchema)]
struct JsonLayer {
name: String,
result: String,
detail: String,
#[serde(skip_serializing_if = "Vec::is_empty")]
warnings: Vec<String>,
}
#[derive(Debug, Serialize, JsonSchema)]
struct JsonFix {
command: String,
impact: u8,
layer: String,
description: String,
}
#[derive(Debug, Serialize, JsonSchema)]
struct JsonDegraded {
layer: String,
reason: String,
}
#[cfg(test)]
#[path = "json_tests.rs"]
mod tests;