use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct SecurityOptions {
pub jailer: bool,
pub landlock: bool,
pub allow_degraded: bool,
}
impl Default for SecurityOptions {
fn default() -> Self {
Self {
jailer: true,
landlock: cfg!(target_os = "linux"),
allow_degraded: false,
}
}
}
impl SecurityOptions {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub const fn landlock(mut self, enable: bool) -> Self {
self.landlock = enable;
self
}
#[must_use]
pub const fn allow_degraded(mut self, yes: bool) -> Self {
self.allow_degraded = yes;
self
}
#[must_use]
pub const fn jailer(mut self, enable: bool) -> Self {
self.jailer = enable;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum LayerStatus {
Enforced,
Degraded,
#[default]
Disabled,
NotApplicable,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[non_exhaustive]
pub struct SecurityStatus {
pub sandbox: String,
pub landlock: LayerStatus,
pub mac: LayerStatus,
}
impl SecurityStatus {
#[cfg(unix)]
#[must_use]
pub fn from_report(r: &bux_jail::SecurityReport) -> Self {
Self {
sandbox: r.sandbox.as_str().to_owned(),
landlock: map_layer(r.landlock),
mac: map_layer(r.mac),
}
}
}
#[cfg(unix)]
const fn map_layer(s: bux_jail::LayerStatus) -> LayerStatus {
match s {
bux_jail::LayerStatus::Enforced => LayerStatus::Enforced,
bux_jail::LayerStatus::Degraded => LayerStatus::Degraded,
bux_jail::LayerStatus::Disabled => LayerStatus::Disabled,
bux_jail::LayerStatus::NotApplicable | _ => LayerStatus::NotApplicable,
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
#[allow(
clippy::struct_excessive_bools,
reason = "independent capability flags"
)]
pub struct HostInfo {
pub virtualization: bool,
pub namespaces: bool,
pub seccomp: bool,
pub mandatory_access_control: bool,
pub cgroups: bool,
pub landlock: bool,
pub max_vcpus: Option<u32>,
pub nested_virt: Option<bool>,
pub krun_features: Vec<String>,
pub isolation_warnings: Vec<String>,
}
impl HostInfo {
#[must_use]
pub fn probe() -> Self {
#[cfg(unix)]
{
let mut caps = bux_jail::checks::check_host();
caps.namespaces = crate::payload::namespaces_available();
Self {
virtualization: caps.virtualization,
namespaces: caps.namespaces,
seccomp: caps.seccomp,
mandatory_access_control: caps.mandatory_access_control,
cgroups: caps.cgroups,
landlock: caps.landlock,
max_vcpus: None,
nested_virt: None,
krun_features: Vec::new(),
isolation_warnings: bux_jail::checks::audit_isolation(&caps),
}
}
#[cfg(not(unix))]
{
Self {
virtualization: false,
namespaces: false,
seccomp: false,
mandatory_access_control: false,
cgroups: false,
landlock: false,
max_vcpus: None,
nested_virt: None,
krun_features: Vec::new(),
isolation_warnings: Vec::new(),
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn defaults_match_platform() {
let s = SecurityOptions::default();
assert!(s.jailer);
assert!(!s.allow_degraded);
assert_eq!(s.landlock, cfg!(target_os = "linux"));
}
#[test]
fn fluent() {
let s = SecurityOptions::new()
.landlock(false)
.allow_degraded(true)
.jailer(false);
assert!(!s.landlock);
assert!(s.allow_degraded);
assert!(!s.jailer);
}
#[test]
fn probe_krun_fields_are_empty() {
let h = HostInfo::probe();
assert!(
h.krun_features.is_empty(),
"engine must not probe libkrun features"
);
assert_eq!(h.max_vcpus, None, "engine must not probe libkrun max_vcpus");
assert_eq!(
h.nested_virt, None,
"engine must not probe libkrun nested_virt"
);
}
#[cfg(target_os = "linux")]
#[test]
#[allow(
clippy::significant_drop_tightening,
reason = "env lock must outlive probe"
)]
fn probe_namespaces_uses_engine_bwrap_lookup() {
let mut env = crate::guest::sidecar_env::lock();
let empty = tempfile::tempdir().unwrap();
env.set("PATH", empty.path());
let planted = crate::guest::sidecar_env::Planted::sibling("bwrap", b"planted-bwrap");
assert!(
planted.path().is_file(),
"sibling bwrap must exist for the engine lookup"
);
assert!(
!bux_jail::checks::check_host().namespaces,
"PATH-only which(bwrap) must miss when PATH is empty"
);
let h = HostInfo::probe();
assert!(
h.namespaces,
"engine lookup must see sibling bwrap: {}",
planted.path().display()
);
assert!(
!h.isolation_warnings
.iter()
.any(|w| w.contains("bubblewrap not found")),
"audit must use overwritten namespaces, got {:?}",
h.isolation_warnings
);
}
}