//! Linux capability management
//!
//! Handles dropping capabilities after they are no longer needed.
//! This follows the principle of least privilege - capabilities are
//! only held while necessary for initialization.
//!
//! # CAP_NET_RAW
//!
//! CAP_NET_RAW is required to create raw sockets for packet capture.
//! However, once the pcap handle is opened, the capability is no longer
//! needed and can be safely dropped. This prevents an attacker from
//! creating new raw sockets if they gain code execution.
//!
//! This is the same pattern used by `ping` and other network utilities.

use anyhow::{Context, Result};
use caps::{CapSet, Capability};

/// Drop CAP_NET_RAW from the current process
///
/// This removes CAP_NET_RAW from both the effective and permitted
/// capability sets. The existing pcap socket file descriptor remains
/// valid since the capability was only needed to create it.
///
/// # Returns
///
/// - `Ok(true)` if CAP_NET_RAW was dropped
/// - `Ok(false)` if CAP_NET_RAW was not held (nothing to drop)
/// - `Err` if dropping failed
pub(crate) fn drop_cap_net_raw() -> Result<bool> {
    // Check if we have CAP_NET_RAW in the effective set
    let has_cap = caps::has_cap(None, CapSet::Effective, Capability::CAP_NET_RAW)
        .context("Failed to check CAP_NET_RAW in effective set")?;

    if !has_cap {
        log::debug!("CAP_NET_RAW not in effective set, nothing to drop");
        return Ok(false);
    }

    // Drop from effective set first
    caps::drop(None, CapSet::Effective, Capability::CAP_NET_RAW)
        .context("Failed to drop CAP_NET_RAW from effective set")?;

    log::debug!("Dropped CAP_NET_RAW from effective set");

    // Also drop from permitted set to prevent re-acquiring
    // This is optional but provides stronger security
    if caps::has_cap(None, CapSet::Permitted, Capability::CAP_NET_RAW).unwrap_or(false) {
        if let Err(e) = caps::drop(None, CapSet::Permitted, Capability::CAP_NET_RAW) {
            // Not fatal - we already dropped from effective
            log::warn!("Could not drop CAP_NET_RAW from permitted set: {}", e);
        } else {
            log::debug!("Dropped CAP_NET_RAW from permitted set");
        }
    }

    Ok(true)
}

/// Check if CAP_NET_RAW is currently held in the effective set
pub(crate) fn has_cap_net_raw() -> bool {
    caps::has_cap(None, CapSet::Effective, Capability::CAP_NET_RAW).unwrap_or(false)
}

/// Drop CAP_BPF and CAP_PERFMON from the current process
///
/// These capabilities are required for loading eBPF programs but are no
/// longer needed once the programs are loaded. Dropping them limits the
/// blast radius if the process is compromised.
///
/// # Returns
///
/// - `Ok(count)` where count is how many capabilities were dropped (0-2)
/// - `Err` if dropping failed
pub(crate) fn drop_ebpf_caps() -> Result<u32> {
    let mut dropped = 0;

    for cap in [Capability::CAP_BPF, Capability::CAP_PERFMON] {
        let has_effective = caps::has_cap(None, CapSet::Effective, cap).unwrap_or(false);

        if !has_effective {
            continue;
        }

        caps::drop(None, CapSet::Effective, cap)
            .with_context(|| format!("Failed to drop {:?} from effective set", cap))?;

        // Also drop from permitted set to prevent re-acquiring
        if caps::has_cap(None, CapSet::Permitted, cap).unwrap_or(false)
            && let Err(e) = caps::drop(None, CapSet::Permitted, cap)
        {
            log::warn!("Could not drop {:?} from permitted set: {}", cap, e);
        }

        log::debug!("Dropped {:?}", cap);
        dropped += 1;
    }

    Ok(dropped)
}

/// Drop CAP_NET_RAW from a worker thread, logging the outcome.
///
/// Linux capabilities are per-thread, so threads spawned before the main
/// thread applies the sandbox keep their own copies and have to drop what they
/// do not use themselves. `thread` names the caller for the log line.
pub fn drop_thread_cap_net_raw(thread: &str) {
    match drop_cap_net_raw() {
        Ok(_) => log::debug!("Dropped CAP_NET_RAW in {thread}"),
        Err(e) => log::warn!("Failed to drop CAP_NET_RAW in {thread}: {e}"),
    }
}

/// Drop CAP_NET_RAW, CAP_BPF and CAP_PERFMON from a worker thread that needs
/// none of them.
///
/// Threads that keep calling `bpf(2)` must use [`drop_thread_cap_net_raw`]
/// instead: with `kernel.unprivileged_bpf_disabled` set, the kernel requires
/// CAP_BPF for *every* `bpf(2)` command, including map lookups on an
/// already-open map file descriptor.
pub fn drop_unused_thread_caps(thread: &str) {
    drop_thread_cap_net_raw(thread);
    match drop_ebpf_caps() {
        Ok(_) => log::debug!("Dropped eBPF capabilities in {thread}"),
        Err(e) => log::warn!("Failed to drop eBPF capabilities in {thread}: {e}"),
    }
}

/// Clear all ambient capabilities
///
/// Ambient capabilities survive `execve()` of non-privileged programs.
/// Clearing them prevents child processes from inheriting any capabilities
/// that were held by the parent. This is standard practice in container
/// runtimes (Docker, systemd) and security-sensitive daemons.
pub(crate) fn clear_ambient_caps() -> Result<()> {
    caps::clear(None, CapSet::Ambient).context("Failed to clear ambient capability set")?;
    log::debug!("Cleared ambient capability set");
    Ok(())
}

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

    #[test]
    fn test_has_cap_net_raw_does_not_panic() {
        // This should not panic regardless of capabilities
        let _ = has_cap_net_raw();
    }

    #[test]
    fn test_drop_cap_net_raw_without_capability() {
        // If we don't have CAP_NET_RAW, drop should return Ok(false)
        // This test may behave differently depending on test environment
        let result = drop_cap_net_raw();
        // Should not error, just return whether it was dropped
        assert!(result.is_ok());
    }

    #[test]
    fn test_drop_ebpf_caps_does_not_panic() {
        // Should not panic regardless of capabilities held
        let result = drop_ebpf_caps();
        assert!(result.is_ok());
    }
}
