#![allow(clippy::unwrap_used, clippy::expect_used)]
mod common;
use common::hive_builder::TestHiveBuilder;
use winreg_artifacts::svc_diff::{classify_service, parse, ServiceEntry};
use winreg_core::hive::Hive;
const SERVICES_KEY: &str = "CurrentControlSet\\Services";
const REG_SZ: u32 = 1;
const REG_DWORD: u32 = 4;
fn reg_sz(s: &str) -> Vec<u8> {
s.encode_utf16().flat_map(u16::to_le_bytes).collect()
}
fn reg_dword(v: u32) -> Vec<u8> {
v.to_le_bytes().to_vec()
}
fn svc_key(name: &str) -> String {
format!("{SERVICES_KEY}\\{name}")
}
#[test]
fn parse_empty_hive_returns_empty() {
let data = TestHiveBuilder::new().build();
let hive = Hive::from_bytes(data).unwrap();
let entries = parse(&hive);
assert!(
entries.is_empty(),
"empty hive (no Services key) should return empty Vec"
);
}
#[test]
fn parse_service_returns_entry() {
let svc = svc_key("Dnscache");
let data = TestHiveBuilder::new()
.add_key(&svc)
.add_value(
&svc,
"ImagePath",
REG_SZ,
®_sz(r"C:\Windows\system32\svchost.exe"),
)
.add_value(&svc, "DisplayName", REG_SZ, ®_sz("DNS Client"))
.add_value(&svc, "Start", REG_DWORD, ®_dword(2))
.add_value(&svc, "Type", REG_DWORD, ®_dword(32))
.add_value(
&svc,
"ObjectName",
REG_SZ,
®_sz("NT AUTHORITY\\NetworkService"),
)
.add_value(&svc, "Description", REG_SZ, ®_sz("Resolves DNS names."))
.build();
let hive = Hive::from_bytes(data).unwrap();
let entries = parse(&hive);
assert!(
!entries.is_empty(),
"hive with one service subkey should return one entry"
);
assert_eq!(entries[0].name, "Dnscache");
}
#[test]
fn parse_image_path_extracted() {
let svc = svc_key("Spooler");
let path = r"C:\Windows\system32\spoolsv.exe";
let data = TestHiveBuilder::new()
.add_key(&svc)
.add_value(&svc, "ImagePath", REG_SZ, ®_sz(path))
.add_value(&svc, "DisplayName", REG_SZ, ®_sz("Print Spooler"))
.add_value(&svc, "Start", REG_DWORD, ®_dword(2))
.add_value(&svc, "Type", REG_DWORD, ®_dword(16))
.add_value(&svc, "ObjectName", REG_SZ, ®_sz("LocalSystem"))
.add_value(&svc, "Description", REG_SZ, ®_sz("Manages print jobs."))
.build();
let hive = Hive::from_bytes(data).unwrap();
let entries = parse(&hive);
assert!(!entries.is_empty());
assert_eq!(
entries[0].image_path, path,
"image_path should equal the ImagePath value"
);
}
#[test]
fn parse_start_type_extracted() {
let svc = svc_key("WSearch");
let data = TestHiveBuilder::new()
.add_key(&svc)
.add_value(
&svc,
"ImagePath",
REG_SZ,
®_sz(r"C:\Windows\system32\SearchIndexer.exe"),
)
.add_value(&svc, "DisplayName", REG_SZ, ®_sz("Windows Search"))
.add_value(&svc, "Start", REG_DWORD, ®_dword(3))
.add_value(&svc, "Type", REG_DWORD, ®_dword(16))
.add_value(&svc, "ObjectName", REG_SZ, ®_sz("LocalSystem"))
.add_value(&svc, "Description", REG_SZ, ®_sz("Provides indexing."))
.build();
let hive = Hive::from_bytes(data).unwrap();
let entries = parse(&hive);
assert!(!entries.is_empty());
assert_eq!(
entries[0].start_type, 3,
"start_type should equal 3 (Manual)"
);
}
#[test]
fn parse_missing_description_captured() {
let svc = svc_key("EvilSvc");
let data = TestHiveBuilder::new()
.add_key(&svc)
.add_value(
&svc,
"ImagePath",
REG_SZ,
®_sz(r"C:\Windows\system32\evil.exe"),
)
.add_value(&svc, "DisplayName", REG_SZ, ®_sz("Evil Service"))
.add_value(&svc, "Start", REG_DWORD, ®_dword(3))
.add_value(&svc, "Type", REG_DWORD, ®_dword(16))
.add_value(&svc, "ObjectName", REG_SZ, ®_sz("LocalSystem"))
.build();
let hive = Hive::from_bytes(data).unwrap();
let entries = parse(&hive);
assert!(!entries.is_empty());
assert_eq!(
entries[0].description, "",
"missing Description value should produce empty string"
);
}
#[test]
fn classify_temp_path_is_suspicious() {
let svc = svc_key("TempSvc");
let data = TestHiveBuilder::new()
.add_key(&svc)
.add_value(
&svc,
"ImagePath",
REG_SZ,
®_sz(r"C:\Windows\Temp\payload.exe"),
)
.add_value(&svc, "DisplayName", REG_SZ, ®_sz("Temp Service"))
.add_value(&svc, "Start", REG_DWORD, ®_dword(2))
.add_value(&svc, "Type", REG_DWORD, ®_dword(16))
.add_value(&svc, "ObjectName", REG_SZ, ®_sz("LocalSystem"))
.add_value(&svc, "Description", REG_SZ, ®_sz("Temp svc."))
.build();
let hive = Hive::from_bytes(data).unwrap();
let entries = parse(&hive);
assert!(!entries.is_empty());
assert!(
entries[0].is_suspicious,
"image path in \\temp\\ should be classified as suspicious"
);
}
#[test]
fn classify_powershell_image_is_suspicious() {
let svc = svc_key("PSSvc");
let data = TestHiveBuilder::new()
.add_key(&svc)
.add_value(
&svc,
"ImagePath",
REG_SZ,
®_sz(r"C:\Windows\system32\powershell.exe -nop"),
)
.add_value(&svc, "DisplayName", REG_SZ, ®_sz("PS Service"))
.add_value(&svc, "Start", REG_DWORD, ®_dword(2))
.add_value(&svc, "Type", REG_DWORD, ®_dword(16))
.add_value(&svc, "ObjectName", REG_SZ, ®_sz("LocalSystem"))
.add_value(&svc, "Description", REG_SZ, ®_sz("PS svc."))
.build();
let hive = Hive::from_bytes(data).unwrap();
let entries = parse(&hive);
assert!(!entries.is_empty());
assert!(
entries[0].is_suspicious,
"image path containing powershell.exe should be suspicious"
);
}
#[test]
fn classify_auto_start_no_description_non_system32_is_suspicious() {
let (is_suspicious, reason) = classify_service(
r"C:\ProgramFiles\Vendor\service.exe",
2, "", "LocalSystem",
);
assert!(
is_suspicious,
"auto-start with no description outside system32 should be suspicious"
);
assert!(reason.is_some());
}
#[test]
fn classify_normal_system32_service_is_benign() {
let (is_suspicious, _reason) = classify_service(
r"C:\Windows\system32\svchost.exe -k netsvcs",
2,
"Resolves and caches DNS names.",
"NT AUTHORITY\\NetworkService",
);
assert!(
!is_suspicious,
"normal system32 auto-start service with description should be benign"
);
}
#[test]
fn parse_multiple_services_returns_all() {
let svc1 = svc_key("Dnscache");
let svc2 = svc_key("Spooler");
let svc3 = svc_key("WSearch");
let data = TestHiveBuilder::new()
.add_key(&svc1)
.add_value(
&svc1,
"ImagePath",
REG_SZ,
®_sz(r"C:\Windows\system32\svchost.exe"),
)
.add_value(&svc1, "Start", REG_DWORD, ®_dword(2))
.add_value(&svc1, "Type", REG_DWORD, ®_dword(32))
.add_value(
&svc1,
"ObjectName",
REG_SZ,
®_sz("NT AUTHORITY\\NetworkService"),
)
.add_value(&svc1, "DisplayName", REG_SZ, ®_sz("DNS Client"))
.add_value(&svc1, "Description", REG_SZ, ®_sz("DNS resolver."))
.add_key(&svc2)
.add_value(
&svc2,
"ImagePath",
REG_SZ,
®_sz(r"C:\Windows\system32\spoolsv.exe"),
)
.add_value(&svc2, "Start", REG_DWORD, ®_dword(2))
.add_value(&svc2, "Type", REG_DWORD, ®_dword(16))
.add_value(&svc2, "ObjectName", REG_SZ, ®_sz("LocalSystem"))
.add_value(&svc2, "DisplayName", REG_SZ, ®_sz("Print Spooler"))
.add_value(&svc2, "Description", REG_SZ, ®_sz("Manages printing."))
.add_key(&svc3)
.add_value(
&svc3,
"ImagePath",
REG_SZ,
®_sz(r"C:\Windows\system32\SearchIndexer.exe"),
)
.add_value(&svc3, "Start", REG_DWORD, ®_dword(3))
.add_value(&svc3, "Type", REG_DWORD, ®_dword(16))
.add_value(&svc3, "ObjectName", REG_SZ, ®_sz("LocalSystem"))
.add_value(&svc3, "DisplayName", REG_SZ, ®_sz("Windows Search"))
.add_value(&svc3, "Description", REG_SZ, ®_sz("Indexing service."))
.build();
let hive = Hive::from_bytes(data).unwrap();
let entries = parse(&hive);
assert_eq!(entries.len(), 3, "should return all 3 service entries");
let names: Vec<&str> = entries.iter().map(|e| e.name.as_str()).collect();
assert!(names.contains(&"Dnscache"), "Dnscache should be in results");
assert!(names.contains(&"Spooler"), "Spooler should be in results");
assert!(names.contains(&"WSearch"), "WSearch should be in results");
}