use anyhow::Result;
use mvm_core::security::{PostureCheck, SecurityLayer, SecurityPolicy};
use mvm_core::time;
use mvm_runtime::shell;
use mvm_security::posture::SecurityPosture;
use crate::ui;
pub fn run(json: bool) -> Result<()> {
let checks = collect_checks();
let report = SecurityPosture::evaluate(checks, &time::utc_now());
if json {
println!("{}", serde_json::to_string_pretty(&report)?);
return Ok(());
}
render_text(&report);
Ok(())
}
fn collect_checks() -> Vec<PostureCheck> {
let vm_available = shell::inside_lima() || vm_is_reachable();
let mut checks = Vec::new();
checks.push(if vm_available {
vm_check(
SecurityLayer::JailerIsolation,
"Jailer binary available",
"command -v jailer >/dev/null 2>&1 && echo yes || echo no",
|out| out.trim() == "yes",
"jailer found",
"jailer not installed",
)
} else {
no_vm_check(SecurityLayer::JailerIsolation, "Jailer binary available")
});
checks.push(if vm_available {
vm_check(
SecurityLayer::SeccompFilter,
"Seccomp strict profile",
"test -f /var/lib/mvm/seccomp/strict.json && echo yes || echo no",
|out| out.trim() == "yes",
"/var/lib/mvm/seccomp/strict.json exists",
"strict seccomp profile not deployed",
)
} else {
no_vm_check(SecurityLayer::SeccompFilter, "Seccomp strict profile")
});
checks.push(if vm_available {
vm_check(
SecurityLayer::NetworkIsolation,
"iptables available",
"command -v iptables 2>/dev/null",
|out| !out.trim().is_empty(),
"iptables found",
"iptables not installed",
)
} else {
no_vm_check(SecurityLayer::NetworkIsolation, "iptables available")
});
checks.push(if vm_available {
vm_check(
SecurityLayer::AuditLogging,
"Audit log directory",
"test -d /var/lib/mvm/tenants && echo yes || echo no",
|out| out.trim() == "yes",
"/var/lib/mvm/tenants/ exists",
"/var/lib/mvm/tenants/ not found",
)
} else {
no_vm_check(SecurityLayer::AuditLogging, "Audit log directory")
});
let policy = SecurityPolicy::default();
checks.push(PostureCheck {
layer: SecurityLayer::VsockAuth,
name: "Vsock auth enabled".to_string(),
passed: policy.require_auth,
detail: if policy.require_auth {
"require_auth is true".to_string()
} else {
"require_auth is false (dev mode default)".to_string()
},
});
checks.push(if vm_available {
vm_check(
SecurityLayer::GuestHardening,
"Built template exists",
"ls /var/lib/mvm/templates/*/current/ 2>/dev/null | head -1",
|out| !out.trim().is_empty(),
"built template found",
"no built templates found",
)
} else {
no_vm_check(SecurityLayer::GuestHardening, "Built template exists")
});
checks
}
fn vm_check(
layer: SecurityLayer,
name: &str,
cmd: &str,
is_ok: impl FnOnce(&str) -> bool,
ok_detail: &str,
fail_detail: &str,
) -> PostureCheck {
let passed = match shell::run_in_vm_stdout(cmd) {
Ok(out) => is_ok(&out),
Err(_) => false,
};
PostureCheck {
layer,
name: name.to_string(),
passed,
detail: if passed {
ok_detail.to_string()
} else {
fail_detail.to_string()
},
}
}
fn no_vm_check(layer: SecurityLayer, name: &str) -> PostureCheck {
PostureCheck {
layer,
name: name.to_string(),
passed: false,
detail: "Lima VM not running".to_string(),
}
}
fn vm_is_reachable() -> bool {
shell::run_in_vm_stdout("echo ok").is_ok()
}
fn render_text(report: &mvm_core::security::PostureReport) {
let total = report.checks.len();
let passed = report.checks.iter().filter(|c| c.passed).count();
let score_line = format!(
"Security Posture: {:.0}% ({}/{} checks passed)",
report.score, passed, total
);
if report.score >= 80.0 {
ui::success(&score_line);
} else if report.score >= 50.0 {
ui::warn(&score_line);
} else {
ui::info(&score_line);
}
println!();
for check in &report.checks {
let tag = layer_tag(&check.layer);
let status = if check.passed { "OK" } else { "FAIL" };
let pad = 40_usize.saturating_sub(check.name.len());
let dots = ".".repeat(pad);
ui::status_line(
&format!(" [{tag}] {} {dots}", check.name),
&format!("{status} ({})", check.detail),
);
}
let uncovered = SecurityPosture::uncovered_layers(&report.checks);
if !uncovered.is_empty() {
let names: Vec<&str> = uncovered.iter().map(|l| layer_name(l)).collect();
println!(
"\n Not evaluated ({} layers): {}",
uncovered.len(),
names.join(", ")
);
}
}
fn layer_tag(layer: &SecurityLayer) -> &'static str {
match layer {
SecurityLayer::JailerIsolation => "JAILER",
SecurityLayer::CgroupLimits => "CGROUP",
SecurityLayer::SeccompFilter => "SECCOMP",
SecurityLayer::NetworkIsolation => "NETWORK",
SecurityLayer::VsockAuth => "VSOCK",
SecurityLayer::EncryptionAtRest => "ENC-REST",
SecurityLayer::EncryptionInTransit => "ENC-TRANSIT",
SecurityLayer::AuditLogging => "AUDIT",
SecurityLayer::SecretManagement => "SECRETS",
SecurityLayer::ConfigImmutability => "CONFIG",
SecurityLayer::GuestHardening => "GUEST",
SecurityLayer::SupplyChainIntegrity => "SUPPLY",
}
}
fn layer_name(layer: &SecurityLayer) -> &'static str {
match layer {
SecurityLayer::JailerIsolation => "JailerIsolation",
SecurityLayer::CgroupLimits => "CgroupLimits",
SecurityLayer::SeccompFilter => "SeccompFilter",
SecurityLayer::NetworkIsolation => "NetworkIsolation",
SecurityLayer::VsockAuth => "VsockAuth",
SecurityLayer::EncryptionAtRest => "EncryptionAtRest",
SecurityLayer::EncryptionInTransit => "EncryptionInTransit",
SecurityLayer::AuditLogging => "AuditLogging",
SecurityLayer::SecretManagement => "SecretManagement",
SecurityLayer::ConfigImmutability => "ConfigImmutability",
SecurityLayer::GuestHardening => "GuestHardening",
SecurityLayer::SupplyChainIntegrity => "SupplyChainIntegrity",
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_posture_check_construction() {
let check = PostureCheck {
layer: SecurityLayer::JailerIsolation,
name: "Jailer binary available".to_string(),
passed: true,
detail: "jailer found".to_string(),
};
assert!(check.passed);
assert_eq!(check.layer, SecurityLayer::JailerIsolation);
}
#[test]
fn test_no_vm_check_always_fails() {
let check = no_vm_check(SecurityLayer::SeccompFilter, "Seccomp strict profile");
assert!(!check.passed);
assert!(check.detail.contains("Lima VM not running"));
}
#[test]
fn test_json_output_valid() {
let checks = vec![
PostureCheck {
layer: SecurityLayer::JailerIsolation,
name: "Jailer binary available".to_string(),
passed: true,
detail: "found".to_string(),
},
PostureCheck {
layer: SecurityLayer::VsockAuth,
name: "Vsock auth enabled".to_string(),
passed: false,
detail: "require_auth is false".to_string(),
},
];
let report = SecurityPosture::evaluate(checks, "2026-02-25T00:00:00Z");
let json = serde_json::to_string_pretty(&report).unwrap();
assert!(json.contains("\"score\""));
assert!(json.contains("JailerIsolation"));
assert!(json.contains("VsockAuth"));
}
#[test]
fn test_vsock_auth_default_requires_auth() {
let policy = SecurityPolicy::default();
assert!(policy.require_auth);
}
#[test]
fn test_vsock_auth_dev_defaults_permissive() {
let policy = SecurityPolicy::dev_defaults();
assert!(!policy.require_auth);
}
#[test]
fn test_layer_tag_coverage() {
for layer in SecurityLayer::all() {
let tag = layer_tag(layer);
assert!(!tag.is_empty());
}
}
#[test]
fn test_layer_name_coverage() {
for layer in SecurityLayer::all() {
let name = layer_name(layer);
assert!(!name.is_empty());
}
}
}