palisade-correlation 2.0.0

Security-conscious correlation engine for Palisade honeypot and deception deployments
//! Fixed-code pattern detection helpers.

use crate::events::{
    EventContext, EventKind, OBSERVED_AUTHENTICATION_FAILURE, OBSERVED_NETWORK_PROBE,
    OBSERVED_PATH_TRAVERSAL, OBSERVED_RAPID_ENUMERATION, ObservedSignal,
};
use crate::matching::contains_ascii_case_insensitive;
use heapless::{Deque, Vec as HVec};

pub(crate) const MAX_PATTERNS_PER_RESULT: usize = 8;
pub(crate) const MAX_PATTERNS_PER_SOURCE: usize = 16;

pub(crate) const PATTERN_BRUTE_FORCE: u16 = 1110;
pub(crate) const PATTERN_DISCOVERY: u16 = 1083;
pub(crate) const PATTERN_CREDENTIAL_ACCESS: u16 = 1078;
pub(crate) const PATTERN_EXPLOITATION: u16 = 1190;
pub(crate) const PATTERN_LATERAL_MOVEMENT: u16 = 1210;
pub(crate) const PATTERN_DENIAL_OF_SERVICE: u16 = 1498;
pub(crate) const PATTERN_COMMAND_AND_CONTROL: u16 = 1071;
pub(crate) const PATTERN_CREDENTIAL_DUMPING: u16 = 1003;
pub(crate) const PATTERN_EXECUTION: u16 = 1059;
pub(crate) const PATTERN_PROCESS_DISCOVERY: u16 = 1057;
pub(crate) const PATTERN_HONEYPOT_PROBING: u16 = 9001;

pub(crate) const KILL_CHAIN_NONE: u8 = 0;
pub(crate) const KILL_CHAIN_RECONNAISSANCE: u8 = 1;
pub(crate) const KILL_CHAIN_WEAPONIZATION: u8 = 2;
pub(crate) const KILL_CHAIN_DELIVERY: u8 = 3;
pub(crate) const KILL_CHAIN_EXPLOITATION: u8 = 4;
pub(crate) const KILL_CHAIN_INSTALLATION: u8 = 5;
pub(crate) const KILL_CHAIN_COMMAND_AND_CONTROL: u8 = 6;
pub(crate) const KILL_CHAIN_ACTIONS_ON_OBJECTIVES: u8 = 7;

pub(crate) fn detect_patterns<const N: usize>(
    event: &EventContext<'_>,
    history: &Deque<ObservedSignal, N>,
    out: &mut HVec<u16, MAX_PATTERNS_PER_RESULT>,
) {
    if is_brute_force(history) {
        push_unique(out, PATTERN_BRUTE_FORCE);
    }

    if is_discovery(history) {
        push_unique(out, PATTERN_DISCOVERY);
    }

    if is_credential_access(&event.kind) {
        push_unique(out, PATTERN_CREDENTIAL_ACCESS);
    }

    if is_exploitation(&event.kind) {
        push_unique(out, PATTERN_EXPLOITATION);
    }

    if is_credential_dumping(&event.kind) {
        push_unique(out, PATTERN_CREDENTIAL_DUMPING);
    }

    if is_command_and_control(&event.kind) {
        push_unique(out, PATTERN_COMMAND_AND_CONTROL);
    }

    if is_execution(&event.kind) {
        push_unique(out, PATTERN_EXECUTION);
    }

    if is_process_discovery(&event.kind) {
        push_unique(out, PATTERN_PROCESS_DISCOVERY);
    }

    if is_dos(history) {
        push_unique(out, PATTERN_DENIAL_OF_SERVICE);
    }

    if is_honeypot_probing(history) {
        push_unique(out, PATTERN_HONEYPOT_PROBING);
    }
}

pub(crate) fn infer_kill_chain_stage(patterns: &[u16]) -> u8 {
    if contains_pattern(patterns, PATTERN_HONEYPOT_PROBING)
        || contains_pattern(patterns, PATTERN_DISCOVERY)
    {
        KILL_CHAIN_RECONNAISSANCE
    } else if contains_pattern(patterns, PATTERN_EXPLOITATION) {
        KILL_CHAIN_EXPLOITATION
    } else if contains_pattern(patterns, PATTERN_COMMAND_AND_CONTROL) {
        KILL_CHAIN_COMMAND_AND_CONTROL
    } else if contains_pattern(patterns, PATTERN_EXECUTION) {
        KILL_CHAIN_INSTALLATION
    } else if contains_pattern(patterns, PATTERN_CREDENTIAL_DUMPING) {
        KILL_CHAIN_ACTIONS_ON_OBJECTIVES
    } else if contains_pattern(patterns, PATTERN_BRUTE_FORCE) {
        KILL_CHAIN_DELIVERY
    } else {
        KILL_CHAIN_NONE
    }
}

pub(crate) fn contains_pattern(patterns: &[u16], target: u16) -> bool {
    patterns.contains(&target)
}

pub(crate) fn push_unique(out: &mut HVec<u16, MAX_PATTERNS_PER_RESULT>, code: u16) {
    if !out.contains(&code) {
        let _ = out.push(code);
    }
}

pub(crate) fn push_unique_campaign_pattern(
    out: &mut HVec<u16, MAX_PATTERNS_PER_SOURCE>,
    code: u16,
) {
    if !out.contains(&code) {
        let _ = out.push(code);
    }
}

fn is_brute_force<const N: usize>(history: &Deque<ObservedSignal, N>) -> bool {
    history
        .iter()
        .filter(|entry| entry.observed_kind == OBSERVED_AUTHENTICATION_FAILURE)
        .count()
        >= 5
}

fn is_discovery<const N: usize>(history: &Deque<ObservedSignal, N>) -> bool {
    let has_scan = history.iter().any(|entry| {
        entry.observed_kind == OBSERVED_RAPID_ENUMERATION
            || entry.observed_kind == OBSERVED_NETWORK_PROBE
    });

    let traversal_count = history
        .iter()
        .filter(|entry| entry.observed_kind == OBSERVED_PATH_TRAVERSAL)
        .count();

    has_scan || traversal_count >= 3
}

fn is_credential_access(event: &EventKind<'_>) -> bool {
    match event {
        EventKind::ArtifactAccess { artifact_id, .. } => {
            contains_ascii_case_insensitive(artifact_id, "cred")
                || contains_ascii_case_insensitive(artifact_id, "key")
                || contains_ascii_case_insensitive(artifact_id, "token")
        }
        _ => false,
    }
}

fn is_exploitation(event: &EventKind<'_>) -> bool {
    matches!(
        event,
        EventKind::SqlInjection { .. }
            | EventKind::CommandInjection { .. }
            | EventKind::PathTraversal { .. }
    )
}

fn is_credential_dumping(event: &EventKind<'_>) -> bool {
    match event {
        EventKind::SuspiciousProcess { process_name, .. } => {
            contains_ascii_case_insensitive(process_name, "mimikatz")
                || contains_ascii_case_insensitive(process_name, "procdump")
                || contains_ascii_case_insensitive(process_name, "lazagne")
                || contains_ascii_case_insensitive(process_name, "secretsdump")
        }
        _ => false,
    }
}

fn is_command_and_control(event: &EventKind<'_>) -> bool {
    matches!(event, EventKind::C2Communication { .. })
}

fn is_execution(event: &EventKind<'_>) -> bool {
    matches!(
        event,
        EventKind::CommandInjection { .. } | EventKind::MalwareDownload { .. }
    )
}

fn is_process_discovery(event: &EventKind<'_>) -> bool {
    matches!(
        event,
        EventKind::SuspiciousProcess { .. } | EventKind::SuspiciousAncestry { .. }
    )
}

fn is_dos<const N: usize>(history: &Deque<ObservedSignal, N>) -> bool {
    if history.len() < 10 {
        return false;
    }

    let scan_count = history
        .iter()
        .filter(|entry| {
            entry.observed_kind == OBSERVED_RAPID_ENUMERATION
                || entry.observed_kind == OBSERVED_NETWORK_PROBE
        })
        .count();

    (scan_count as f64 / history.len() as f64) > 0.8
}

fn is_honeypot_probing<const N: usize>(history: &Deque<ObservedSignal, N>) -> bool {
    if history.len() < 8 {
        return false;
    }

    let mut unique = HVec::<u8, 16>::new();
    for entry in history {
        if !unique.contains(&entry.observed_kind) {
            let _ = unique.push(entry.observed_kind);
        }
    }

    unique.len() >= 4
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::events::{EventContext, EventKind};

    #[test]
    fn test_brute_force_pattern() {
        let mut history = Deque::<ObservedSignal, 16>::new();
        for _ in 0..5 {
            history
                .push_back(ObservedSignal::new(OBSERVED_AUTHENTICATION_FAILURE, 1))
                .unwrap();
        }

        let event = EventContext::new(
            "127.0.0.1".parse().unwrap(),
            "session-1",
            70.0,
            EventKind::AuthenticationFailure {
                username: "admin",
                method: "password",
            },
        )
        .unwrap();

        let mut detected = HVec::<u16, MAX_PATTERNS_PER_RESULT>::new();
        detect_patterns(&event, &history, &mut detected);
        assert!(detected.contains(&PATTERN_BRUTE_FORCE));
    }

    #[test]
    fn test_kill_chain_inference() {
        let patterns = [PATTERN_DISCOVERY, PATTERN_HONEYPOT_PROBING];
        assert_eq!(infer_kill_chain_stage(&patterns), KILL_CHAIN_RECONNAISSANCE);
    }
}