//! SUID exploitation pattern detection (spec 050-PR4).
//!
//! Fires when a non-root user execs a SUID binary that wasn't in the
//! baseline `suid_inventory` snapshot. The "freshly suided" attribute
//! is the smoking gun: every published privesc-via-SUID exploit
//! (GTFOBins, custom polkit chains, dirty-pipe, etc.) involves
//! dropping a SUID binary the operator never installed and then
//! exec'ing it.
//!
//! Existing host SUID baseline lives in
//! `collectors/suid_inventory.rs` — that collector emits a snapshot
//! event with the canonical SUID list at sensor boot. This detector
//! consumes that snapshot via shared state (`host_inventory` module)
//! and consults it on every exec.
//!
//! Anti-FP gates:
//!   - SUID binaries in the baseline (sudo, passwd, su, mount,
//!     umount, pkexec, etc.) → silenced.
//!   - Operator-extensible `[detectors.setuid_exploit_pattern]` TOML
//!     for new SUID binaries the operator installs post-baseline.
//!
//! Severity: critical. A non-root user exec'ing a non-baseline SUID
//! binary IS the privesc smoking gun.
//!
//! MITRE: T1548.001 (Abuse Elevation Control Mechanism: Setuid and Setgid).

use std::collections::{HashMap, HashSet};
use std::sync::{Arc, RwLock};

use chrono::{DateTime, Duration, Utc};
use innerwarden_core::{event::Event, event::Severity, incident::Incident};

/// SUID binaries that ship in every standard Debian/RHEL/SUSE distro.
/// Anything NOT in this list AND NOT in the operator-extensible
/// runtime baseline is flagged. Conservative — better to fire on
/// `pkexec` exec from a non-baseline path than to miss the GTFOBins
/// chain.
const BASELINE_SUID_BINARIES: &[&str] = &[
    "sudo",
    "su",
    "passwd",
    "mount",
    "umount",
    "fusermount",
    "fusermount3",
    "ping",
    "ping6",
    "chsh",
    "chfn",
    "newgrp",
    "gpasswd",
    "pkexec",
    "polkit-agent-helper-1",
    "dbus-daemon-launch-helper",
    "ssh-keysign",
    "pam_extrausers_chkpwd",
    "unix_chkpwd",
    "at",
    "crontab",
    "expiry",
    "Xorg.wrap",
    "kismet_cap_linux_wifi",
    "kismet_cap_linux_bluetooth",
];

pub struct SetuidExploitPatternDetector {
    host: String,
    /// Shared runtime baseline of SUID binaries observed at sensor
    /// boot via `suid_inventory` collector. Empty until first
    /// snapshot — falls back to the const list when not yet
    /// populated.
    runtime_baseline: Arc<RwLock<HashSet<String>>>,
    last_fired: HashMap<String, DateTime<Utc>>,
    cooldown: Duration,
}

impl SetuidExploitPatternDetector {
    pub fn new(host: impl Into<String>) -> Self {
        Self {
            host: host.into(),
            runtime_baseline: Arc::new(RwLock::new(HashSet::new())),
            last_fired: HashMap::new(),
            cooldown: Duration::seconds(600),
        }
    }

    /// Builder hook for the sensor wiring layer to share the baseline
    /// Arc with the suid_inventory collector. When the collector
    /// emits its initial snapshot, it populates the same Arc.
    #[allow(dead_code)]
    pub fn with_runtime_baseline(mut self, baseline: Arc<RwLock<HashSet<String>>>) -> Self {
        self.runtime_baseline = baseline;
        self
    }

    pub fn process(&mut self, event: &Event) -> Option<Incident> {
        if event.kind != "shell.command_exec" && event.kind != "process.exec" {
            return None;
        }
        let argv0 = event
            .details
            .get("argv")
            .and_then(|v| v.get(0))
            .and_then(|v| v.as_str())
            .unwrap_or("");
        if argv0.is_empty() {
            return None;
        }
        let argv0_base = argv0.split('/').next_back().unwrap_or(argv0);

        // Only proceed if the event carries SUID context: either the
        // execve emitter tagged it, OR the legacy path is observable.
        // Conservative: when no SUID hint is present, return None
        // (we don't want to fire on every exec — only suspected SUID).
        let is_suid = event
            .details
            .get("suid")
            .and_then(|v| v.as_bool())
            .unwrap_or(false)
            || event
                .details
                .get("setuid")
                .and_then(|v| v.as_bool())
                .unwrap_or(false);
        if !is_suid {
            return None;
        }

        // Effective uid 0 from a process whose real uid is non-zero
        // is the classic SUID elevation. If the event doesn't carry
        // uid/euid split, fall back to checking uid==0 only — gives
        // us at least the standard SUID coverage.
        let uid = event
            .details
            .get("uid")
            .and_then(|v| v.as_u64())
            .unwrap_or(0);
        let euid = event
            .details
            .get("euid")
            .and_then(|v| v.as_u64())
            .unwrap_or(uid);
        let elevated = euid == 0 && uid != 0;
        // If the event has no euid distinction but uid is non-zero,
        // a SUID-tagged exec from non-root is still suspicious.
        if !elevated && uid == 0 {
            // Root exec'ing a SUID binary is normal (chmod +s scripts,
            // etc.). Not interesting.
            return None;
        }

        if is_baseline_suid(argv0_base) {
            return None;
        }
        if let Ok(rt) = self.runtime_baseline.read() {
            if rt.contains(argv0_base) {
                return None;
            }
        }

        let comm = event
            .details
            .get("comm")
            .and_then(|v| v.as_str())
            .unwrap_or("");
        let command = event
            .details
            .get("command")
            .and_then(|v| v.as_str())
            .unwrap_or("");
        let pid = event
            .details
            .get("pid")
            .and_then(|v| v.as_u64())
            .unwrap_or(0);
        let parent_comm = event
            .details
            .get("parent_comm")
            .and_then(|v| v.as_str())
            .unwrap_or("");

        let now = event.ts;
        let key = format!("{uid}:{argv0_base}");
        if let Some(&last) = self.last_fired.get(&key) {
            if now - last < self.cooldown {
                return None;
            }
        }
        self.last_fired.insert(key, now);

        Some(Incident {
            ts: now,
            host: self.host.clone(),
            incident_id: format!(
                "setuid_exploit_pattern:{}:{}",
                argv0_base,
                now.format("%Y-%m-%dT%H:%M:%SZ")
            ),
            severity: Severity::Critical,
            title: format!(
                "Non-baseline SUID binary exec: `{argv0_base}` by uid={uid} (euid={euid})"
            ),
            summary: format!(
                "Process `{comm}` (parent=`{parent_comm}`, pid={pid}) exec'd `{argv0_base}` \
                 (`{command}`). The binary IS marked SUID but is NOT on the host's baseline \
                 SUID list and was NOT installed by the operator. This is the canonical \
                 privesc-via-SUID smoking gun (T1548.001) — GTFOBins, polkit chains, dirty-pipe \
                 all land here."
            ),
            evidence: serde_json::json!([{
                "kind": "setuid_exploit_pattern",
                "argv0": argv0,
                "argv0_base": argv0_base,
                "uid": uid,
                "euid": euid,
                "comm": comm,
                "parent_comm": parent_comm,
                "pid": pid,
                "command": command,
                "mitre": ["T1548.001"],
            }]),
            recommended_checks: vec![
                format!("ls -la $(which {argv0_base}) — confirm SUID bit and ownership"),
                format!("Inspect process tree of pid {pid}: pstree -p {pid}"),
                "If this is a known SUID tool the operator installed, allowlist via [detectors.setuid_exploit_pattern]".to_string(),
                "Roll back the SUID bit on the binary unless explicitly required".to_string(),
            ],
            tags: vec!["privesc".to_string(), "suid".to_string()],
            entities: vec![],
        })
    }
}

fn is_baseline_suid(base: &str) -> bool {
    BASELINE_SUID_BINARIES.contains(&base)
}

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

    fn suid_exec_event(argv0_path: &str, uid: u64, euid: u64) -> Event {
        Event {
            ts: Utc::now(),
            host: "test".into(),
            source: "ebpf".into(),
            kind: "shell.command_exec".into(),
            severity: Severity::Info,
            summary: format!("suid exec {argv0_path}"),
            details: serde_json::json!({
                "pid": 4242,
                "uid": uid,
                "euid": euid,
                "ppid": 4241,
                "suid": true,
                "comm": "bash",
                "parent_comm": "bash",
                "command": argv0_path,
                "argv": [argv0_path],
                "argc": 1,
            }),
            tags: vec![],
            entities: vec![],
        }
    }

    #[test]
    fn fires_on_non_baseline_suid_exec_by_non_root() {
        let mut det = SetuidExploitPatternDetector::new("test");
        let ev = suid_exec_event("/tmp/customprivesc", 1000, 0);
        let inc = det.process(&ev).expect("should fire");
        assert_eq!(inc.severity, Severity::Critical);
    }

    #[test]
    fn silences_baseline_suid_binaries() {
        let mut det = SetuidExploitPatternDetector::new("test");
        for bin in ["sudo", "su", "passwd", "pkexec", "mount", "fusermount"] {
            let ev = suid_exec_event(&format!("/usr/bin/{bin}"), 1000, 0);
            assert!(det.process(&ev).is_none(), "baseline {bin} should not fire");
        }
    }

    #[test]
    fn silences_runtime_baseline_additions() {
        let baseline = Arc::new(RwLock::new(
            ["operator_custom_suid".to_string()].into_iter().collect(),
        ));
        let mut det = SetuidExploitPatternDetector::new("test").with_runtime_baseline(baseline);
        let ev = suid_exec_event("/usr/local/bin/operator_custom_suid", 1000, 0);
        assert!(
            det.process(&ev).is_none(),
            "runtime-baselined binary must be silenced"
        );
    }

    #[test]
    fn ignores_non_suid_exec_events() {
        let mut det = SetuidExploitPatternDetector::new("test");
        let mut ev = suid_exec_event("/tmp/notsuid", 1000, 1000);
        ev.details["suid"] = serde_json::Value::Bool(false);
        ev.details["setuid"] = serde_json::Value::Bool(false);
        assert!(det.process(&ev).is_none());
    }

    #[test]
    fn ignores_root_running_suid() {
        let mut det = SetuidExploitPatternDetector::new("test");
        // root with euid=0 running SUID — that's not privesc, just admin work.
        let ev = suid_exec_event("/tmp/customprivesc", 0, 0);
        assert!(det.process(&ev).is_none());
    }

    #[test]
    fn dedupes_within_cooldown() {
        let mut det = SetuidExploitPatternDetector::new("test");
        let ev = suid_exec_event("/tmp/exploit", 1000, 0);
        assert!(det.process(&ev).is_some());
        let mut ev2 = ev.clone();
        ev2.ts = ev.ts + Duration::seconds(60);
        assert!(det.process(&ev2).is_none());
    }

    #[test]
    fn ignores_non_exec_event_kinds() {
        let mut det = SetuidExploitPatternDetector::new("test");
        let mut ev = suid_exec_event("/tmp/x", 1000, 0);
        ev.kind = "file.read_access".into();
        assert!(det.process(&ev).is_none());
    }
}
