whyno-cli 0.5.0

Linux permission debugger
use whyno_core::checks::{run_checks, CheckReport};
use whyno_core::fix::{generate_fixes, FixPlan};
use whyno_core::operation::{MetadataParams, Operation};
use whyno_core::test_helpers::StateBuilder;

use crate::output::json;

fn render_to_string(
    report: &CheckReport,
    plan: &FixPlan,
    state: &whyno_core::state::SystemState,
) -> String {
    let mut buf = Vec::new();
    json::render(report, plan, state, &mut buf).expect("render should succeed");
    String::from_utf8(buf).expect("output should be valid UTF-8")
}

#[test]
fn all_pass_json() {
    let state = StateBuilder::new()
        .subject(1000, 1000, vec![])
        .operation(Operation::Read)
        .component("/", 0, 0, 0o755)
        .component_file("/home/file.txt", 1000, 1000, 0o644)
        .mount("/", "ext4", "rw")
        .build();
    let report = run_checks(&state, &MetadataParams::default());
    let plan = generate_fixes(&report, &state, &MetadataParams::default());
    let output = render_to_string(&report, &plan, &state);

    // verify it parses as valid JSON.
    let _: serde_json::Value = serde_json::from_str(&output).expect("output must be valid JSON");

    insta::assert_snapshot!(output);
}

#[test]
fn failure_with_fixes_json() {
    let state = StateBuilder::new()
        .subject(33, 33, vec![])
        .operation(Operation::Read)
        .component("/", 0, 0, 0o755)
        .component("/var", 0, 0, 0o755)
        .component_file("/var/log.txt", 0, 0, 0o640)
        .mount("/", "ext4", "rw")
        .build();
    let report = run_checks(&state, &MetadataParams::default());
    let plan = generate_fixes(&report, &state, &MetadataParams::default());
    let output = render_to_string(&report, &plan, &state);

    let parsed: serde_json::Value =
        serde_json::from_str(&output).expect("output must be valid JSON");
    assert_eq!(parsed["version"], 1);
    assert_eq!(parsed["result"], "denied");

    insta::assert_snapshot!(output);
}

#[test]
fn nosuid_warning_in_json() {
    let state = StateBuilder::new()
        .subject(1000, 1000, vec![])
        .operation(Operation::Execute)
        .component("/", 0, 0, 0o755)
        .component_file("/mnt/suid_bin", 0, 0, 0o4755)
        .mount("/mnt", "ext4", "nosuid")
        .build();
    let report = run_checks(&state, &MetadataParams::default());
    let plan = generate_fixes(&report, &state, &MetadataParams::default());
    let output = render_to_string(&report, &plan, &state);

    let parsed: serde_json::Value =
        serde_json::from_str(&output).expect("output must be valid JSON");
    let mount_layer = &parsed["layers"][0];
    assert_eq!(mount_layer["result"], "pass");
    assert!(
        mount_layer["warnings"]
            .as_array()
            .expect("warnings must be array")
            .len()
            == 1
    );

    insta::assert_snapshot!(output);
}

#[test]
fn schema_output_is_stable() {
    let schema = json::generate_schema();
    let output = serde_json::to_string_pretty(&schema).expect("schema should serialize");
    insta::assert_snapshot!(output);
}

#[test]
fn degraded_layers_in_json() {
    let state = StateBuilder::new()
        .subject(33, 33, vec![])
        .operation(Operation::Read)
        .component("/", 0, 0, 0o755)
        .component_inaccessible("/secret")
        .mount("/", "ext4", "rw")
        .build();
    let report = run_checks(&state, &MetadataParams::default());
    let plan = generate_fixes(&report, &state, &MetadataParams::default());
    let output = render_to_string(&report, &plan, &state);

    let parsed: serde_json::Value =
        serde_json::from_str(&output).expect("output must be valid JSON");
    assert!(!parsed["degraded"]
        .as_array()
        .expect("degraded must be array")
        .is_empty());

    insta::assert_snapshot!(output);
}