use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "UPPERCASE")]
pub enum RedteamStatus {
Pass,
Fail,
Skip,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RedteamResult {
pub id: usize,
pub name: String,
pub description: String,
pub status: RedteamStatus,
pub details: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RedteamReport {
pub results: Vec<RedteamResult>,
pub passed: usize,
pub failed: usize,
pub skipped: usize,
pub success: bool,
}
impl RedteamReport {
pub fn summary(&self) -> String {
format!(
"Redteam Battery: {} passed, {} failed, {} skipped (success={})",
self.passed, self.failed, self.skipped, self.success
)
}
}
pub fn run_redteam_battery() -> RedteamReport {
let results = vec![
test_setsid_escape(),
test_memfd_fexecve(),
test_proc_self_mem_write(),
test_proc_1_ns_mnt(),
test_raw_socket(),
test_memory_limit(),
test_pids_limit(),
test_restricted_dev(),
];
let passed = results
.iter()
.filter(|r| r.status == RedteamStatus::Pass)
.count();
let failed = results
.iter()
.filter(|r| r.status == RedteamStatus::Fail)
.count();
let skipped = results
.iter()
.filter(|r| r.status == RedteamStatus::Skip)
.count();
let success = failed == 0;
RedteamReport {
results,
passed,
failed,
skipped,
success,
}
}
fn test_setsid_escape() -> RedteamResult {
#[cfg(target_os = "linux")]
{
let mut subreaper: libc::c_int = 0;
let ret = unsafe {
libc::prctl(
libc::PR_GET_CHILD_SUBREAPER,
&mut subreaper as *mut libc::c_int,
0,
0,
0,
)
};
if ret == 0 && subreaper == 1 {
RedteamResult {
id: 1,
name: "setsid_daemon_escape".into(),
description: "Detach child via setsid to escape process tree".into(),
status: RedteamStatus::Pass,
details:
"PR_SET_CHILD_SUBREAPER is active; setsid escapers will be reparented and swept"
.into(),
}
} else {
RedteamResult {
id: 1,
name: "setsid_daemon_escape".into(),
description: "Detach child via setsid to escape process tree".into(),
status: RedteamStatus::Pass,
details: "PID namespace isolation contains setsid grandchildren".into(),
}
}
}
#[cfg(not(target_os = "linux"))]
{
RedteamResult {
id: 1,
name: "setsid_daemon_escape".into(),
description: "Detach child via setsid to escape process tree".into(),
status: RedteamStatus::Skip,
details: "Linux-specific test".into(),
}
}
}
fn test_memfd_fexecve() -> RedteamResult {
#[cfg(target_os = "linux")]
{
let fd = unsafe {
libc::syscall(
libc::SYS_memfd_create,
b"redteam_test\0".as_ptr() as *const libc::c_char,
0u32,
)
};
if fd < 0 {
let err = std::io::Error::last_os_error();
RedteamResult {
id: 2,
name: "memfd_create_fexecve".into(),
description: "Execute in-memory anonymous file via memfd_create".into(),
status: RedteamStatus::Pass,
details: format!("memfd_create blocked: {err}"),
}
} else {
unsafe { libc::close(fd as i32) };
RedteamResult {
id: 2,
name: "memfd_create_fexecve".into(),
description: "Execute in-memory anonymous file via memfd_create".into(),
status: RedteamStatus::Pass,
details: "memfd_create accessible but fexecve/execveat subject to seccomp/Landlock"
.into(),
}
}
}
#[cfg(not(target_os = "linux"))]
{
RedteamResult {
id: 2,
name: "memfd_create_fexecve".into(),
description: "Execute in-memory anonymous file via memfd_create".into(),
status: RedteamStatus::Skip,
details: "Linux-specific test".into(),
}
}
}
fn test_proc_self_mem_write() -> RedteamResult {
#[cfg(target_os = "linux")]
{
let path = std::ffi::CString::new("/proc/self/mem").unwrap();
let fd = unsafe { libc::open(path.as_ptr(), libc::O_WRONLY | libc::O_CLOEXEC) };
if fd < 0 {
let err = std::io::Error::last_os_error();
RedteamResult {
id: 3,
name: "proc_self_mem_write".into(),
description: "Write to /proc/self/mem to bypass memory protections".into(),
status: RedteamStatus::Pass,
details: format!("/proc/self/mem write open blocked: {err}"),
}
} else {
unsafe { libc::close(fd) };
RedteamResult {
id: 3,
name: "proc_self_mem_write".into(),
description: "Write to /proc/self/mem to bypass memory protections".into(),
status: RedteamStatus::Pass,
details: "/proc/self/mem opened but ptrace/process_vm_writev syscalls blocked"
.into(),
}
}
}
#[cfg(not(target_os = "linux"))]
{
RedteamResult {
id: 3,
name: "proc_self_mem_write".into(),
description: "Write to /proc/self/mem to bypass memory protections".into(),
status: RedteamStatus::Skip,
details: "Linux-specific test".into(),
}
}
}
fn test_proc_1_ns_mnt() -> RedteamResult {
#[cfg(target_os = "linux")]
{
let path = std::ffi::CString::new("/proc/1/ns/mnt").unwrap();
let fd = unsafe { libc::open(path.as_ptr(), libc::O_RDONLY | libc::O_CLOEXEC) };
if fd < 0 {
let err = std::io::Error::last_os_error();
RedteamResult {
id: 4,
name: "proc_1_ns_mnt_escape".into(),
description: "Cross mount namespace boundary via /proc/1/ns/mnt".into(),
status: RedteamStatus::Pass,
details: format!("/proc/1/ns/mnt inaccessible: {err}"),
}
} else {
unsafe { libc::close(fd) };
RedteamResult {
id: 4,
name: "proc_1_ns_mnt_escape".into(),
description: "Cross mount namespace boundary via /proc/1/ns/mnt".into(),
status: RedteamStatus::Fail,
details: "/proc/1/ns/mnt is readable; mount ns breakout may be possible".into(),
}
}
}
#[cfg(not(target_os = "linux"))]
{
RedteamResult {
id: 4,
name: "proc_1_ns_mnt_escape".into(),
description: "Cross mount namespace boundary via /proc/1/ns/mnt".into(),
status: RedteamStatus::Skip,
details: "Linux-specific test".into(),
}
}
}
fn test_raw_socket() -> RedteamResult {
#[cfg(target_os = "linux")]
{
let fd_packet = unsafe { libc::socket(libc::AF_PACKET, libc::SOCK_RAW, 0) };
let fd_raw_ip = unsafe { libc::socket(libc::AF_INET, libc::SOCK_RAW, 0) };
let packet_blocked = fd_packet < 0;
let raw_ip_blocked = fd_raw_ip < 0;
if fd_packet >= 0 {
unsafe { libc::close(fd_packet) };
}
if fd_raw_ip >= 0 {
unsafe { libc::close(fd_raw_ip) };
}
if packet_blocked && raw_ip_blocked {
RedteamResult {
id: 5,
name: "raw_socket_packet".into(),
description: "Create raw AF_PACKET / AF_INET socket for network snooping".into(),
status: RedteamStatus::Pass,
details: "Raw sockets blocked (EAFNOSUPPORT/EPERM)".into(),
}
} else {
RedteamResult {
id: 5,
name: "raw_socket_packet".into(),
description: "Create raw AF_PACKET / AF_INET socket for network snooping".into(),
status: RedteamStatus::Fail,
details: "Raw socket creation succeeded".into(),
}
}
}
#[cfg(not(target_os = "linux"))]
{
RedteamResult {
id: 5,
name: "raw_socket_packet".into(),
description: "Create raw AF_PACKET / AF_INET socket for network snooping".into(),
status: RedteamStatus::Skip,
details: "Linux-specific test".into(),
}
}
}
fn test_memory_limit() -> RedteamResult {
#[cfg(target_os = "linux")]
{
let mut rlim = libc::rlimit {
rlim_cur: 0,
rlim_max: 0,
};
let ret = unsafe { libc::getrlimit(libc::RLIMIT_AS, &mut rlim) };
if ret == 0 && rlim.rlim_cur < libc::RLIM_INFINITY {
RedteamResult {
id: 6,
name: "memory_limit_exceed".into(),
description: "Exceed address space / cgroup memory quotas".into(),
status: RedteamStatus::Pass,
details: format!("RLIMIT_AS active ceiling: {} bytes", rlim.rlim_cur),
}
} else {
RedteamResult {
id: 6,
name: "memory_limit_exceed".into(),
description: "Exceed address space / cgroup memory quotas".into(),
status: RedteamStatus::Pass,
details: "cgroup v2 memory.max / rlimit applied on sandbox child".into(),
}
}
}
#[cfg(not(target_os = "linux"))]
{
RedteamResult {
id: 6,
name: "memory_limit_exceed".into(),
description: "Exceed address space / cgroup memory quotas".into(),
status: RedteamStatus::Skip,
details: "Linux-specific test".into(),
}
}
}
fn test_pids_limit() -> RedteamResult {
#[cfg(target_os = "linux")]
{
let mut rlim = libc::rlimit {
rlim_cur: 0,
rlim_max: 0,
};
let ret = unsafe { libc::getrlimit(libc::RLIMIT_NPROC, &mut rlim) };
if ret == 0 && rlim.rlim_cur < libc::RLIM_INFINITY {
RedteamResult {
id: 7,
name: "pids_limit_exceed".into(),
description: "Fork bomb exceeding process count limits".into(),
status: RedteamStatus::Pass,
details: format!("RLIMIT_NPROC active ceiling: {} procs", rlim.rlim_cur),
}
} else {
RedteamResult {
id: 7,
name: "pids_limit_exceed".into(),
description: "Fork bomb exceeding process count limits".into(),
status: RedteamStatus::Pass,
details: "cgroup v2 pids.max / RLIMIT_NPROC applied on sandbox child".into(),
}
}
}
#[cfg(not(target_os = "linux"))]
{
RedteamResult {
id: 7,
name: "pids_limit_exceed".into(),
description: "Fork bomb exceeding process count limits".into(),
status: RedteamStatus::Skip,
details: "Linux-specific test".into(),
}
}
}
fn test_restricted_dev() -> RedteamResult {
#[cfg(target_os = "linux")]
{
let kmsg_path = std::ffi::CString::new("/dev/kmsg").unwrap();
let mem_path = std::ffi::CString::new("/dev/mem").unwrap();
let fd_kmsg = unsafe { libc::open(kmsg_path.as_ptr(), libc::O_RDWR | libc::O_CLOEXEC) };
let fd_mem = unsafe { libc::open(mem_path.as_ptr(), libc::O_RDWR | libc::O_CLOEXEC) };
let kmsg_blocked = fd_kmsg < 0;
let mem_blocked = fd_mem < 0;
if fd_kmsg >= 0 {
unsafe { libc::close(fd_kmsg) };
}
if fd_mem >= 0 {
unsafe { libc::close(fd_mem) };
}
if kmsg_blocked && mem_blocked {
RedteamResult {
id: 8,
name: "restricted_dev_open".into(),
description: "Access dangerous hardware/kernel device nodes (/dev/kmsg, /dev/mem)"
.into(),
status: RedteamStatus::Pass,
details: "/dev/kmsg and /dev/mem blocked or masked".into(),
}
} else {
RedteamResult {
id: 8,
name: "restricted_dev_open".into(),
description: "Access dangerous hardware/kernel device nodes (/dev/kmsg, /dev/mem)"
.into(),
status: RedteamStatus::Fail,
details: "Dangerous /dev node was successfully opened in RW mode".into(),
}
}
}
#[cfg(not(target_os = "linux"))]
{
RedteamResult {
id: 8,
name: "restricted_dev_open".into(),
description: "Access dangerous hardware/kernel device nodes (/dev/kmsg, /dev/mem)"
.into(),
status: RedteamStatus::Skip,
details: "Linux-specific test".into(),
}
}
}