use std::os::fd::{FromRawFd, OwnedFd};
use crate::events::{bus::EventBus, Event};
pub const NETLINK_AUDIT: libc::c_int = 9;
pub fn open_audit_feed() -> Result<OwnedFd, String> {
let fd = unsafe {
libc::socket(
libc::AF_NETLINK,
libc::SOCK_RAW | libc::SOCK_CLOEXEC,
NETLINK_AUDIT,
)
};
if fd < 0 {
return Err(format!(
"audit netlink socket: {} (needs CAP_AUDIT_READ / auditd; kernel >= 6.12 for landlock denials)",
std::io::Error::last_os_error()
));
}
#[repr(C)]
struct SockAddrNl {
nl_family: libc::sa_family_t,
nl_pad: u16,
nl_pid: u32,
nl_groups: u32,
}
let addr = SockAddrNl {
nl_family: libc::AF_NETLINK as libc::sa_family_t,
nl_pad: 0,
nl_pid: 0,
nl_groups: 1, };
let r = unsafe {
libc::bind(
fd,
&addr as *const SockAddrNl as *const libc::sockaddr,
std::mem::size_of::<SockAddrNl>() as u32,
)
};
if r != 0 {
let err = std::io::Error::last_os_error();
unsafe { libc::close(fd) };
return Err(format!("audit bind: {err}"));
}
Ok(unsafe { OwnedFd::from_raw_fd(fd) })
}
pub fn spawn_reader_if_available(bus: EventBus) -> Option<String> {
let fd = match open_audit_feed() {
Ok(fd) => fd,
Err(reason) => return Some(reason),
};
std::thread::Builder::new()
.name("vetto-audit".into())
.spawn(move || {
let file = std::fs::File::from(fd);
let mut reader = std::io::BufReader::new(file);
use std::io::BufRead;
loop {
let mut line = String::new();
match reader.read_line(&mut line) {
Ok(0) => break,
Ok(_) => {
let lower = line.to_ascii_lowercase();
if lower.contains("landlock") && lower.contains("denied") {
bus.publish(Event::BlockedAttempt {
ts: crate::events::types::now(),
pid: parse_audit_pid(&line),
comm: "?".into(),
path: extract_denied_path(&line).unwrap_or_default(),
source: "kernel-audit".into(),
});
}
}
Err(_) => break,
}
}
})
.expect("spawn audit reader");
None
}
fn parse_audit_pid(line: &str) -> u32 {
line.split_whitespace()
.find_map(|f| f.strip_prefix("pid="))
.and_then(|p| p.parse().ok())
.unwrap_or(0)
}
fn extract_denied_path(line: &str) -> Option<String> {
let idx = line.find("path=\"")? + 6;
let rest = &line[idx..];
Some(rest[..rest.find('"')?].to_string())
}