butterfly-bot 0.8.0

Butterfly Bot is an opinionated personal-ops AI assistant built for people who want results, not setup overhead.
Documentation
use crate::error::{ButterflyBotError, Result};
use std::fmt;
use zeroize::Zeroize;

#[derive(Debug, Clone)]
pub struct StartupComplianceReport {
    pub strict_profile: bool,
    pub page_locking_ready: bool,
    pub core_dump_protection_ready: bool,
    pub ptrace_protection_ready: bool,
}

impl StartupComplianceReport {
    pub fn is_compliant(&self) -> bool {
        self.strict_profile
            && self.page_locking_ready
            && self.core_dump_protection_ready
            && self.ptrace_protection_ready
    }
}

impl fmt::Display for StartupComplianceReport {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "strict_profile={}, page_locking_ready={}, core_dump_protection_ready={}, ptrace_protection_ready={}, compliant={}",
            self.strict_profile,
            self.page_locking_ready,
            self.core_dump_protection_ready,
            self.ptrace_protection_ready,
            self.is_compliant()
        )
    }
}

pub struct SensitiveBuffer {
    bytes: Vec<u8>,
    locked: bool,
}

impl SensitiveBuffer {
    pub fn from_vec(bytes: Vec<u8>) -> Result<Self> {
        if !bytes.is_empty() {
            lock_bytes(&bytes)?;
        }
        Ok(Self {
            bytes,
            locked: true,
        })
    }

    pub fn expose<R>(&self, f: impl FnOnce(&[u8]) -> R) -> R {
        f(&self.bytes)
    }
}

impl Drop for SensitiveBuffer {
    fn drop(&mut self) {
        if self.locked && !self.bytes.is_empty() {
            let _ = unlock_bytes(&self.bytes);
        }
        self.bytes.zeroize();
        self.locked = false;
    }
}

pub fn with_sensitive_string<T, F>(secret: String, f: F) -> Result<T>
where
    F: FnOnce(&str) -> Result<T>,
{
    let buffer = SensitiveBuffer::from_vec(secret.into_bytes())?;
    let text = std::str::from_utf8(buffer.bytes.as_slice()).map_err(|e| {
        ButterflyBotError::SecurityPolicy(format!("invalid utf-8 in sensitive string: {e}"))
    })?;
    f(text)
}

pub fn run_startup_self_check() -> Result<StartupComplianceReport> {
    let report = StartupComplianceReport {
        strict_profile: true,
        page_locking_ready: verify_page_locking()?,
        core_dump_protection_ready: enforce_core_dump_protection()?,
        ptrace_protection_ready: enforce_ptrace_protection()?,
    };

    tracing::info!(
        target: "security",
        event = "strict_profile_startup_self_check",
        strict_profile = report.strict_profile,
        page_locking_ready = report.page_locking_ready,
        core_dump_protection_ready = report.core_dump_protection_ready,
        ptrace_protection_ready = report.ptrace_protection_ready,
        compliant = report.is_compliant(),
        "security startup self-check complete"
    );

    if !report.is_compliant() {
        return Err(ButterflyBotError::SecurityPolicy(
            "Strict profile compliance self-check failed".to_string(),
        ));
    }

    Ok(report)
}

fn verify_page_locking() -> Result<bool> {
    let sample = vec![0u8; 4096];
    lock_bytes(&sample)?;
    unlock_bytes(&sample)?;
    Ok(true)
}

#[cfg(any(target_os = "linux", target_os = "macos"))]
fn enforce_core_dump_protection() -> Result<bool> {
    let mut current = libc::rlimit {
        rlim_cur: 0,
        rlim_max: 0,
    };

    let get_before = unsafe { libc::getrlimit(libc::RLIMIT_CORE, &mut current) };
    if get_before != 0 {
        return Err(ButterflyBotError::SecurityPolicy(
            "failed to read RLIMIT_CORE".to_string(),
        ));
    }

    let hardened = libc::rlimit {
        rlim_cur: 0,
        rlim_max: current.rlim_max,
    };

    let set_result = unsafe { libc::setrlimit(libc::RLIMIT_CORE, &hardened) };
    if set_result != 0 {
        return Err(ButterflyBotError::SecurityPolicy(
            "failed to disable core dumps".to_string(),
        ));
    }

    let mut after = libc::rlimit {
        rlim_cur: 0,
        rlim_max: 0,
    };
    let get_after = unsafe { libc::getrlimit(libc::RLIMIT_CORE, &mut after) };
    if get_after != 0 || after.rlim_cur != 0 {
        return Err(ButterflyBotError::SecurityPolicy(
            "core dump protection verification failed".to_string(),
        ));
    }

    Ok(true)
}

#[cfg(target_os = "windows")]
fn enforce_core_dump_protection() -> Result<bool> {
    use windows_sys::Win32::System::Diagnostics::Debug::{
        SetErrorMode, SEM_FAILCRITICALERRORS, SEM_NOGPFAULTERRORBOX, SEM_NOOPENFILEERRORBOX,
    };

    let _previous = unsafe {
        SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX | SEM_NOOPENFILEERRORBOX)
    };
    Ok(true)
}

#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
fn enforce_core_dump_protection() -> Result<bool> {
    Err(ButterflyBotError::SecurityPolicy(
        "strict profile requires platform core dump protection controls".to_string(),
    ))
}

#[cfg(target_os = "linux")]
fn enforce_ptrace_protection() -> Result<bool> {
    let deny_ptrace = unsafe { libc::prctl(libc::PR_SET_DUMPABLE, 0, 0, 0, 0) };
    if deny_ptrace != 0 {
        return Err(ButterflyBotError::SecurityPolicy(
            "failed to set ptrace protection (PR_SET_DUMPABLE=0)".to_string(),
        ));
    }

    let dumpable = unsafe { libc::prctl(libc::PR_GET_DUMPABLE, 0, 0, 0, 0) };
    if dumpable != 0 {
        return Err(ButterflyBotError::SecurityPolicy(
            "ptrace protection verification failed".to_string(),
        ));
    }

    Ok(true)
}

#[cfg(target_os = "macos")]
fn enforce_ptrace_protection() -> Result<bool> {
    let deny_ptrace = unsafe { libc::ptrace(libc::PT_DENY_ATTACH, 0, std::ptr::null_mut(), 0) };
    if deny_ptrace != 0 {
        return Err(ButterflyBotError::SecurityPolicy(
            "failed to set ptrace protection (PT_DENY_ATTACH)".to_string(),
        ));
    }

    Ok(true)
}

#[cfg(target_os = "windows")]
fn enforce_ptrace_protection() -> Result<bool> {
    use windows_sys::Win32::System::Diagnostics::Debug::IsDebuggerPresent;

    let debugger_present = unsafe { IsDebuggerPresent() } != 0;
    if debugger_present {
        return Err(ButterflyBotError::SecurityPolicy(
            "failed to enforce anti-debug protection (debugger present)".to_string(),
        ));
    }

    Ok(true)
}

#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
fn enforce_ptrace_protection() -> Result<bool> {
    Err(ButterflyBotError::SecurityPolicy(
        "strict profile requires platform ptrace protection controls".to_string(),
    ))
}

#[cfg(any(target_os = "linux", target_os = "macos"))]
fn lock_bytes(value: &[u8]) -> Result<()> {
    if value.is_empty() {
        return Ok(());
    }
    let rc = unsafe { libc::mlock(value.as_ptr() as *const libc::c_void, value.len()) };
    if rc != 0 {
        return Err(ButterflyBotError::SecurityPolicy(
            "strict profile requires page locking for sensitive buffers".to_string(),
        ));
    }
    Ok(())
}

#[cfg(target_os = "windows")]
fn lock_bytes(value: &[u8]) -> Result<()> {
    use windows_sys::Win32::System::Memory::VirtualLock;

    if value.is_empty() {
        return Ok(());
    }
    let rc = unsafe { VirtualLock(value.as_ptr() as *const core::ffi::c_void, value.len()) };
    if rc == 0 {
        return Err(ButterflyBotError::SecurityPolicy(
            "strict profile requires page locking for sensitive buffers".to_string(),
        ));
    }
    Ok(())
}

#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
fn lock_bytes(_value: &[u8]) -> Result<()> {
    Err(ButterflyBotError::SecurityPolicy(
        "strict profile requires page locking on this platform".to_string(),
    ))
}

#[cfg(any(target_os = "linux", target_os = "macos"))]
fn unlock_bytes(value: &[u8]) -> Result<()> {
    if value.is_empty() {
        return Ok(());
    }
    let rc = unsafe { libc::munlock(value.as_ptr() as *const libc::c_void, value.len()) };
    if rc != 0 {
        return Err(ButterflyBotError::Runtime(
            "failed to unlock sensitive buffer".to_string(),
        ));
    }
    Ok(())
}

#[cfg(target_os = "windows")]
fn unlock_bytes(value: &[u8]) -> Result<()> {
    use windows_sys::Win32::System::Memory::VirtualUnlock;

    if value.is_empty() {
        return Ok(());
    }
    let rc = unsafe { VirtualUnlock(value.as_ptr() as *const core::ffi::c_void, value.len()) };
    if rc == 0 {
        return Err(ButterflyBotError::Runtime(
            "failed to unlock sensitive buffer".to_string(),
        ));
    }
    Ok(())
}

#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
fn unlock_bytes(_value: &[u8]) -> Result<()> {
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn sensitive_string_scopes_plaintext_to_closure() {
        let result = with_sensitive_string("phase-g-secret".to_string(), |secret| {
            assert_eq!(secret, "phase-g-secret");
            Ok(secret.len())
        })
        .unwrap();

        assert_eq!(result, 14);
    }

    #[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))]
    #[test]
    fn startup_self_check_reports_strict_compliance() {
        let report = run_startup_self_check().unwrap();
        assert!(report.strict_profile);
        assert!(report.page_locking_ready);
        assert!(report.core_dump_protection_ready);
        assert!(report.ptrace_protection_ready);
        assert!(report.is_compliant());
    }
}