whyno-cli 0.5.0

Linux permission debugger
//! Structured JSON output (schema v1). Output structs decouple the public schema from internal types.

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(),
    }
}

// "allowed" / "denied" / "degraded" (allowed but at least one layer skipped)
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,
    /// Advisory warnings that do not block the operation.
    #[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;