use std::sync::LazyLock;
use regex::Regex;
static RE_MEM_AUTO_INIT: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"(?i)mem auto-init:\s*stack:(\S+?),\s*heap alloc:(\S+?),\s*heap free:(\S+?)\s*$")
.unwrap()
});
static RE_KASLR_OFF: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"(?i)\bKASLR disabled(?:\s+due to\s+(.+?))?\s*$").unwrap());
static RE_KASLR_ON: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"(?i)\bKASLR enabled\b").unwrap());
static RE_LSM_LIST: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"(?i)\bLSM:\s*initializing\s+lsm=(\S+)").unwrap());
static RE_APPARMOR_OFF: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"(?i)AppArmor:\s*AppArmor disabled by boot time parameter").unwrap()
});
static RE_SELINUX_STATE: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"(?i)SELinux:\s*(Initializing|Permissive|Enforcing|Disabled at runtime)").unwrap()
});
static RE_UNKNOWN_PARAMS: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"(?i)Unknown command line parameters:\s*(.+?)\s*$").unwrap());
static RE_CMDLINE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"(?i)(?:Kernel command line|cmdline|bootargs)\s*[:=]").unwrap());
static RE_SELINUX_OFF: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"(?i)\bselinux=0\b").unwrap());
const MAC_MODULES: &[&str] = &["selinux", "apparmor", "smack", "tomoyo"];
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct MemAutoInit {
pub stack: String,
pub heap_alloc: String,
pub heap_free: String,
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct OsHardening {
pub mem_auto_init: Option<MemAutoInit>,
pub kaslr: Option<String>,
pub kaslr_reason: Option<String>,
pub lsm: Vec<String>,
pub mac_modules: Vec<String>,
pub selinux: Option<String>,
pub apparmor: Option<String>,
pub ignored_kernel_parameters: Option<String>,
}
impl OsHardening {
pub fn is_empty(&self) -> bool {
*self == Self::default()
}
}
fn clip(s: &str, max: usize) -> String {
s.chars().take(max).collect()
}
pub fn parse(log: &str) -> OsHardening {
let mut h = OsHardening::default();
for raw in log.lines() {
let line = raw.trim_end_matches(['\r', '\n']);
if let Some(caps) = RE_UNKNOWN_PARAMS.captures(line) {
let ignored = caps[1].to_string();
if h.ignored_kernel_parameters.is_none() {
h.ignored_kernel_parameters = Some(clip(&ignored, 300));
}
if RE_SELINUX_OFF.is_match(&ignored) && h.selinux.is_none() {
h.selinux = Some("not_supported".to_string());
}
continue;
}
if let Some(caps) = RE_MEM_AUTO_INIT.captures(line) {
if h.mem_auto_init.is_none() {
h.mem_auto_init = Some(MemAutoInit {
stack: caps[1].trim().to_string(),
heap_alloc: caps[2].trim().to_string(),
heap_free: caps[3].trim().to_string(),
});
continue;
}
}
if let Some(caps) = RE_KASLR_OFF.captures(line) {
if h.kaslr.is_none() {
h.kaslr = Some("disabled".to_string());
let reason = caps.get(1).map(|m| m.as_str().trim()).unwrap_or_default();
if !reason.is_empty() {
h.kaslr_reason = Some(clip(reason, 120));
}
continue;
}
}
if RE_KASLR_ON.is_match(line) && h.kaslr.is_none() {
h.kaslr = Some("enabled".to_string());
continue;
}
if let Some(caps) = RE_LSM_LIST.captures(line) {
if h.lsm.is_empty() {
h.lsm = caps[1]
.split(',')
.map(|x| x.trim().to_ascii_lowercase())
.filter(|x| !x.is_empty())
.collect();
let mut mac: Vec<String> = h
.lsm
.iter()
.filter(|m| MAC_MODULES.contains(&m.as_str()))
.cloned()
.collect();
mac.sort();
mac.dedup();
h.mac_modules = mac;
continue;
}
}
if RE_APPARMOR_OFF.is_match(line) && h.apparmor.is_none() {
h.apparmor = Some("disabled_by_parameter".to_string());
continue;
}
if let Some(caps) = RE_SELINUX_STATE.captures(line) {
if h.selinux.is_none() {
h.selinux = Some(caps[1].to_ascii_lowercase());
continue;
}
}
if RE_CMDLINE.is_match(line) && RE_SELINUX_OFF.is_match(line) && h.selinux.is_none() {
h.selinux = Some("disabled_by_parameter".to_string());
}
}
h
}