use std::io::Cursor;
use winreg_core::hive::Hive;
const SERVICES_KEY: &str = "CurrentControlSet\\Services";
#[derive(Debug, Clone, serde::Serialize)]
pub struct ServiceEntry {
pub name: String,
pub display_name: String,
pub image_path: String,
pub start_type: u32,
pub service_type: u32,
pub object_name: String,
pub description: String,
pub is_suspicious: bool,
pub suspicious_reason: Option<String>,
pub last_written: Option<chrono::DateTime<chrono::Utc>>,
}
pub fn classify_service(
image_path: &str,
start_type: u32,
description: &str,
object_name: &str,
) -> (bool, Option<String>) {
let lower = image_path.to_ascii_lowercase();
for suspect_dir in &[r"\temp\", r"\appdata\", r"\users\public\", r"\programdata\"] {
if lower.contains(suspect_dir) {
return (
true,
Some(format!(
"image path is in user-writable directory: {suspect_dir}"
)),
);
}
}
for interpreter in &["cmd.exe", "powershell.exe", "wscript.exe", "mshta.exe"] {
if lower.contains(interpreter) {
return (
true,
Some(format!("image path contains interpreter: {interpreter}")),
);
}
}
if start_type == 2
&& description.is_empty()
&& !lower.contains(r"\system32\")
&& !lower.contains(r"\syswow64\")
{
return (
true,
Some(
"auto-start service has no description and image path is not under \\system32\\ or \\syswow64\\"
.to_string(),
),
);
}
if object_name.is_empty() {
return (
true,
Some("service has no configured account (ObjectName is empty)".to_string()),
);
}
(false, None)
}
pub fn parse(hive: &Hive<Cursor<Vec<u8>>>) -> Vec<ServiceEntry> {
let active = hive
.open_key("Select")
.ok()
.flatten()
.and_then(|k| k.value("Current").ok().flatten())
.and_then(|v| v.as_u32().ok())
.unwrap_or(1);
let services_key = [
format!("ControlSet{active:03}\\Services"),
"ControlSet001\\Services".to_string(),
SERVICES_KEY.to_string(),
]
.iter()
.find_map(|path| hive.open_key(path).ok().flatten());
let Some(services_key) = services_key else {
return Vec::new();
};
let Ok(subkeys) = services_key.subkeys() else {
return Vec::new();
};
let mut entries = Vec::with_capacity(subkeys.len());
for svc_key in subkeys {
let name = svc_key.name();
let image_path = svc_key
.value("ImagePath")
.ok()
.flatten()
.and_then(|v| v.as_string().ok())
.unwrap_or_default();
let display_name = svc_key
.value("DisplayName")
.ok()
.flatten()
.and_then(|v| v.as_string().ok())
.unwrap_or_default();
let description = svc_key
.value("Description")
.ok()
.flatten()
.and_then(|v| v.as_string().ok())
.unwrap_or_default();
let start_type = svc_key
.value("Start")
.ok()
.flatten()
.and_then(|v| v.as_u32().ok())
.unwrap_or(3);
let service_type = svc_key
.value("Type")
.ok()
.flatten()
.and_then(|v| v.as_u32().ok())
.unwrap_or(0);
let object_name = svc_key
.value("ObjectName")
.ok()
.flatten()
.and_then(|v| v.as_string().ok())
.unwrap_or_default();
let (is_suspicious, suspicious_reason) =
classify_service(&image_path, start_type, &description, &object_name);
entries.push(ServiceEntry {
name,
display_name,
image_path,
start_type,
service_type,
object_name,
description,
is_suspicious,
suspicious_reason,
last_written: svc_key.last_written(),
});
}
entries
}