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 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 services_key = match hive.open_key(SERVICES_KEY) {
Ok(Some(k)) => k,
_ => return Vec::new(),
};
let subkeys = match services_key.subkeys() {
Ok(k) => k,
Err(_) => 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,
});
}
entries
}