764 lines
27 KiB
Rust
764 lines
27 KiB
Rust
#[cfg(windows)]
|
|
use proxywarden_lib::component_detection::SystemProxyfierDetectionHost;
|
|
use proxywarden_lib::component_detection::{
|
|
detect_proxyfier_install_with_host, detect_singbox_install_with_host,
|
|
has_additional_matching_legacy_proxifyre_service_with_host, inventory_proxyfier_with_host,
|
|
inventory_singbox_with_host, matches_legacy_proxifyre_2_2_1_manifest,
|
|
proxyfier_component_from_detection, proxyfier_component_from_inventory,
|
|
service_executable_from_path_name, singbox_component_from_detection, LegacyPackageFileIdentity,
|
|
ProxyfierDetectionHost, ProxyfierEngine, RegistryInstallEntry, LEGACY_PROXIFYRE_2_2_1_MANIFEST,
|
|
};
|
|
use proxywarden_lib::component_inventory::{
|
|
BinaryIdentityEvidence, ComponentClassification, OWNERSHIP_MISMATCH,
|
|
};
|
|
use proxywarden_lib::models::ComponentState;
|
|
use std::{
|
|
collections::{HashMap, HashSet},
|
|
path::{Path, PathBuf},
|
|
};
|
|
|
|
#[test]
|
|
fn detects_existing_proxifyre_from_registry_install_location() {
|
|
let host = MockHost::new()
|
|
.with_registry("ProxiFyre", r"C:\Tools\ProxiFyre")
|
|
.with_path(r"C:\Tools\ProxiFyre")
|
|
.with_path(r"C:\Tools\ProxiFyre\ProxiFyre.exe")
|
|
.with_service_path(
|
|
"ProxiFyreService",
|
|
r#""C:\Tools\ProxiFyre\ProxiFyre.exe" --service"#,
|
|
)
|
|
.with_known_binary(r"C:\Tools\ProxiFyre\ProxiFyre.exe")
|
|
.with_version(r"C:\Tools\ProxiFyre\ProxiFyre.exe", "2.2.1.0");
|
|
|
|
let detected = detect_proxyfier_install_with_host(&host)
|
|
.expect("existing ProxiFyre install should be detected");
|
|
|
|
assert_eq!(detected.engine, ProxyfierEngine::ProxiFyre);
|
|
assert_eq!(detected.install_dir, PathBuf::from(r"C:\Tools\ProxiFyre"));
|
|
assert_eq!(
|
|
detected.config_path,
|
|
Some(PathBuf::from(r"C:\Tools\ProxiFyre\app-config.json"))
|
|
);
|
|
assert!(detected.running);
|
|
assert_eq!(detected.service_name, Some("ProxiFyreService".to_string()));
|
|
assert_eq!(detected.service_status, Some("running".to_string()));
|
|
assert_eq!(detected.version, Some("2.2.1.0".to_string()));
|
|
|
|
let component = proxyfier_component_from_detection(Some(&detected));
|
|
assert_eq!(component.state, ComponentState::Running);
|
|
assert!(component.installed);
|
|
assert!(component.running);
|
|
assert_eq!(component.path, Some(r"C:\Tools\ProxiFyre".to_string()));
|
|
assert_eq!(component.service_name, Some("ProxiFyreService".to_string()));
|
|
assert_eq!(component.service_status, Some("running".to_string()));
|
|
assert_eq!(component.version, Some("2.2.1.0".to_string()));
|
|
assert!(component.problems.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn detects_current_proxifyre_only_with_strong_marker_and_exact_service_path() {
|
|
let root = r"C:\Program Files\ProxyWarden\components\ProxiFyre";
|
|
let executable = r"C:\Program Files\ProxyWarden\components\ProxiFyre\ProxiFyre.exe";
|
|
let marker = serde_json::json!({
|
|
"manager": "ProxyWarden",
|
|
"component": "proxifyre",
|
|
"serviceName": "ProxiFyreService",
|
|
"installRoot": root,
|
|
"packetFilterInstalledByProxyWarden": false
|
|
})
|
|
.to_string();
|
|
let host = MockHost::new()
|
|
.with_path(root)
|
|
.with_path(executable)
|
|
.with_text(
|
|
r"C:\Program Files\ProxyWarden\components\ProxiFyre\proxywarden-component.json",
|
|
&marker,
|
|
)
|
|
.with_stopped_service_path(
|
|
"ProxiFyreService",
|
|
r#""C:\Program Files\ProxyWarden\components\ProxiFyre\ProxiFyre.exe" --service"#,
|
|
)
|
|
.with_version(executable, "2.4.0.0");
|
|
|
|
let detected = detect_proxyfier_install_with_host(&host).expect("managed current ProxiFyre");
|
|
assert_eq!(detected.install_dir, PathBuf::from(root));
|
|
assert_eq!(detected.version, Some("2.4.0.0".to_string()));
|
|
}
|
|
|
|
#[test]
|
|
fn current_proxifyre_with_foreign_same_name_service_is_ownership_mismatch() {
|
|
let root = r"C:\Program Files\ProxyWarden\components\ProxiFyre";
|
|
let executable = r"C:\Program Files\ProxyWarden\components\ProxiFyre\ProxiFyre.exe";
|
|
let marker = serde_json::json!({
|
|
"manager": "ProxyWarden",
|
|
"component": "proxifyre",
|
|
"serviceName": "ProxiFyreService",
|
|
"installRoot": root,
|
|
"packetFilterInstalledByProxyWarden": false
|
|
})
|
|
.to_string();
|
|
let host = MockHost::new()
|
|
.with_path(root)
|
|
.with_path(executable)
|
|
.with_text(
|
|
r"C:\Program Files\ProxyWarden\components\ProxiFyre\proxywarden-component.json",
|
|
&marker,
|
|
)
|
|
.with_service_path(
|
|
"ProxiFyreService",
|
|
r#""C:\Foreign\ProxiFyre.exe" --service"#,
|
|
);
|
|
|
|
let inventory = inventory_proxyfier_with_host(&host);
|
|
assert_eq!(inventory.classification(), ComponentClassification::Foreign);
|
|
assert_eq!(
|
|
inventory.selected_candidate().unwrap().issues[0].code,
|
|
OWNERSHIP_MISMATCH
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn ignores_empty_common_proxifyre_folder_without_executable() {
|
|
let host = MockHost::new().with_path(r"C:\Tools\ProxiFyre");
|
|
|
|
assert!(detect_proxyfier_install_with_host(&host).is_none());
|
|
|
|
let component = proxyfier_component_from_detection(None);
|
|
assert_eq!(component.state, ComponentState::Missing);
|
|
assert!(!component.installed);
|
|
}
|
|
|
|
#[test]
|
|
fn ignores_plain_proxifier_install() {
|
|
let host = MockHost::new()
|
|
.with_registry("Proxifier", r"C:\Program Files\Proxifier")
|
|
.with_path(r"C:\Program Files\Proxifier")
|
|
.with_process("Proxifier.exe");
|
|
|
|
assert!(detect_proxyfier_install_with_host(&host).is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn env_override_does_not_make_portable_proxifyre_managed() {
|
|
let host = MockHost::new()
|
|
.with_env("PROXYWARDEN_PROXIFYRE_ROOT", r"D:\Portable\ProxiFyre")
|
|
.with_path(r"D:\Portable\ProxiFyre\ProxiFyre.exe");
|
|
|
|
assert!(detect_proxyfier_install_with_host(&host).is_none());
|
|
let inventory = inventory_proxyfier_with_host(&host);
|
|
assert_eq!(inventory.classification(), ComponentClassification::Foreign);
|
|
}
|
|
|
|
#[test]
|
|
fn reports_stopped_proxifyre_service_when_executable_exists() {
|
|
let host = MockHost::new()
|
|
.with_env("PROXYWARDEN_PROXIFYRE_ROOT", r"C:\Tools\ProxiFyre")
|
|
.with_path(r"C:\Tools\ProxiFyre\ProxiFyre.exe")
|
|
.with_stopped_service_path(
|
|
"ProxiFyreService",
|
|
r#""C:\Tools\ProxiFyre\ProxiFyre.exe" --service"#,
|
|
)
|
|
.with_known_binary(r"C:\Tools\ProxiFyre\ProxiFyre.exe");
|
|
|
|
let detected =
|
|
detect_proxyfier_install_with_host(&host).expect("proxifyre executable should be detected");
|
|
let component = proxyfier_component_from_detection(Some(&detected));
|
|
|
|
assert_eq!(component.state, ComponentState::Installed);
|
|
assert!(component.installed);
|
|
assert!(!component.running);
|
|
assert_eq!(component.service_name, Some("ProxiFyreService".to_string()));
|
|
assert_eq!(component.service_status, Some("stopped".to_string()));
|
|
assert!(component.problems.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn primary_and_alias_same_root_remain_discoverable_but_cutover_is_ambiguous() {
|
|
let root = Path::new(r"C:\Tools\ProxiFyre");
|
|
let executable = root.join("ProxiFyre.exe");
|
|
let host = MockHost::new()
|
|
.with_path(executable.to_str().expect("fixture path"))
|
|
.with_service_path(
|
|
"ProxiFyreService",
|
|
r#""C:\Tools\ProxiFyre\ProxiFyre.exe" -displayname "ProxiFyre Service" -servicename ProxiFyreService"#,
|
|
)
|
|
.with_service_path(
|
|
"ProxiFyre",
|
|
r#""C:\Tools\ProxiFyre\ProxiFyre.exe" --service"#,
|
|
)
|
|
.with_known_binary(executable.to_str().expect("fixture path"));
|
|
|
|
assert_eq!(
|
|
inventory_proxyfier_with_host(&host).classification(),
|
|
ComponentClassification::ManagedLegacy,
|
|
"Task 5 discovery/Start/Stop classification stays unchanged"
|
|
);
|
|
assert!(has_additional_matching_legacy_proxifyre_service_with_host(
|
|
&host, root
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn any_present_alias_service_makes_the_strict_service_set_ambiguous() {
|
|
let root = Path::new(r"C:\Tools\ProxiFyre");
|
|
for host in [
|
|
MockHost::new().with_service("ProxiFyre"),
|
|
MockHost::new().with_service_path("ProxiFyre", r#""C:\Foreign\ProxiFyre.exe" --service"#),
|
|
] {
|
|
assert!(has_additional_matching_legacy_proxifyre_service_with_host(
|
|
&host, root
|
|
));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn missing_proxyfier_returns_install_action_status() {
|
|
let component = proxyfier_component_from_detection(None);
|
|
|
|
assert_eq!(component.state, ComponentState::Missing);
|
|
assert!(!component.installed);
|
|
assert_eq!(component.actions, vec!["Установить ProxiFyre"]);
|
|
}
|
|
|
|
#[test]
|
|
fn ignores_known_service_name_when_path_points_to_foreign_binary() {
|
|
let host = MockHost::new()
|
|
.with_env("PROXYWARDEN_PROXIFYRE_ROOT", r"C:\Tools\ProxiFyre")
|
|
.with_path(r"C:\Tools\ProxiFyre\ProxiFyre.exe")
|
|
.with_service_path(
|
|
"ProxiFyreService",
|
|
r#""C:\Foreign\ProxiFyre.exe" --service"#,
|
|
);
|
|
|
|
assert!(detect_proxyfier_install_with_host(&host).is_none());
|
|
let inventory = inventory_proxyfier_with_host(&host);
|
|
let candidate = inventory.selected_candidate().expect("foreign collision");
|
|
assert_eq!(candidate.classification, ComponentClassification::Foreign);
|
|
assert_eq!(candidate.issues[0].code, OWNERSHIP_MISMATCH);
|
|
let component = proxyfier_component_from_inventory(&inventory);
|
|
assert_eq!(component.state, ComponentState::Error);
|
|
assert!(!component.running);
|
|
assert!(component.actions.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn ignores_known_service_name_without_path_metadata() {
|
|
let host = MockHost::new()
|
|
.with_env("PROXYWARDEN_PROXIFYRE_ROOT", r"C:\Tools\ProxiFyre")
|
|
.with_path(r"C:\Tools\ProxiFyre\ProxiFyre.exe")
|
|
.with_service("ProxiFyreService");
|
|
|
|
assert!(detect_proxyfier_install_with_host(&host).is_none());
|
|
let inventory = inventory_proxyfier_with_host(&host);
|
|
assert_eq!(inventory.classification(), ComponentClassification::Foreign);
|
|
assert_eq!(
|
|
inventory.selected_candidate().unwrap().issues[0].code,
|
|
OWNERSHIP_MISMATCH
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn detects_running_local_singbox_from_default_install_root_and_service() {
|
|
let host = MockHost::new()
|
|
.with_path(r"C:\Program Files\ProxyWarden\components\sing-box\sing-box.exe")
|
|
.with_path(r"C:\Program Files\ProxyWarden\components\sing-box\ProxyWardenSingBox.exe")
|
|
.with_text(
|
|
r"C:\Program Files\ProxyWarden\components\sing-box\ProxyWardenSingBox.xml",
|
|
winsw_xml(),
|
|
)
|
|
.with_service_path(
|
|
"ProxyWardenSingBox",
|
|
r#""C:\Program Files\ProxyWarden\components\sing-box\ProxyWardenSingBox.exe""#,
|
|
)
|
|
.with_version(
|
|
r"C:\Program Files\ProxyWarden\components\sing-box\sing-box.exe",
|
|
"1.11.0.0",
|
|
);
|
|
|
|
let detected =
|
|
detect_singbox_install_with_host(&host).expect("existing sing-box should be detected");
|
|
|
|
assert_eq!(
|
|
detected.executable_path,
|
|
PathBuf::from(r"C:\Program Files\ProxyWarden\components\sing-box\sing-box.exe")
|
|
);
|
|
assert_eq!(detected.service_name, "ProxyWardenSingBox");
|
|
assert!(detected.running);
|
|
assert_eq!(detected.version, Some("1.11.0.0".to_string()));
|
|
|
|
let component = singbox_component_from_detection(Some(&detected));
|
|
assert_eq!(component.state, ComponentState::Running);
|
|
assert!(component.installed);
|
|
assert!(component.running);
|
|
assert_eq!(
|
|
component.path,
|
|
Some(r"C:\Program Files\ProxyWarden\components\sing-box\sing-box.exe".to_string())
|
|
);
|
|
assert!(component.problems.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn current_singbox_with_foreign_same_name_service_is_ownership_mismatch() {
|
|
let host = MockHost::new()
|
|
.with_path(r"C:\Program Files\ProxyWarden\components\sing-box\sing-box.exe")
|
|
.with_path(r"C:\Program Files\ProxyWarden\components\sing-box\ProxyWardenSingBox.exe")
|
|
.with_text(
|
|
r"C:\Program Files\ProxyWarden\components\sing-box\ProxyWardenSingBox.xml",
|
|
winsw_xml(),
|
|
)
|
|
.with_service_path(
|
|
"ProxyWardenSingBox",
|
|
r#""C:\Foreign\ProxyWardenSingBox.exe""#,
|
|
);
|
|
|
|
let inventory = inventory_singbox_with_host(&host);
|
|
assert_eq!(inventory.classification(), ComponentClassification::Foreign);
|
|
assert_eq!(
|
|
inventory.selected_candidate().unwrap().issues[0].code,
|
|
OWNERSHIP_MISMATCH
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn winsw_identity_cannot_be_spoofed_by_comments_or_unrelated_nodes() {
|
|
let spoofed_xml = r#"<service>
|
|
<!-- <id>ProxyWardenSingBox</id> -->
|
|
<!-- <executable>%BASE%\sing-box.exe</executable> -->
|
|
<metadata><arguments>run -c "%BASE%\config.json"</arguments></metadata>
|
|
<id>ForeignService</id>
|
|
<executable>C:\Foreign\sing-box.exe</executable>
|
|
<arguments>run -c "C:\Foreign\config.json"</arguments>
|
|
</service>"#;
|
|
let host = MockHost::new()
|
|
.with_path(r"C:\Program Files\ProxyWarden\components\sing-box\sing-box.exe")
|
|
.with_path(r"C:\Program Files\ProxyWarden\components\sing-box\ProxyWardenSingBox.exe")
|
|
.with_text(
|
|
r"C:\Program Files\ProxyWarden\components\sing-box\ProxyWardenSingBox.xml",
|
|
spoofed_xml,
|
|
)
|
|
.with_service_path(
|
|
"ProxyWardenSingBox",
|
|
r#""C:\Program Files\ProxyWarden\components\sing-box\ProxyWardenSingBox.exe""#,
|
|
);
|
|
|
|
let inventory = inventory_singbox_with_host(&host);
|
|
assert_eq!(
|
|
inventory.classification(),
|
|
ComponentClassification::Incomplete
|
|
);
|
|
assert!(detect_singbox_install_with_host(&host).is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn winsw_identity_rejects_duplicate_dtd_cdata_second_root_and_oversized_xml() {
|
|
let oversized = format!(
|
|
"<service><id>ProxyWardenSingBox</id><executable>%BASE%\\sing-box.exe</executable><arguments>run -c \"%BASE%\\config.json\"</arguments><description>{}</description></service>",
|
|
"x".repeat(65 * 1024)
|
|
);
|
|
let invalid_xml = vec![
|
|
r#"<service><id>ProxyWardenSingBox</id><id>ProxyWardenSingBox</id><executable>%BASE%\sing-box.exe</executable><arguments>run -c "%BASE%\config.json"</arguments></service>"#.to_string(),
|
|
r#"<!DOCTYPE service [<!ENTITY owned "ProxyWardenSingBox">]><service><id>&owned;</id><executable>%BASE%\sing-box.exe</executable><arguments>run -c "%BASE%\config.json"</arguments></service>"#.to_string(),
|
|
r#"<service><id><![CDATA[ProxyWardenSingBox]]></id><executable>%BASE%\sing-box.exe</executable><arguments>run -c "%BASE%\config.json"</arguments></service>"#.to_string(),
|
|
format!("{}<service></service>", winsw_xml()),
|
|
oversized,
|
|
];
|
|
|
|
for xml in invalid_xml {
|
|
let host = current_singbox_host_with_xml(&xml);
|
|
assert!(
|
|
detect_singbox_install_with_host(&host).is_none(),
|
|
"unsafe WinSW XML was accepted"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn winsw_identity_accepts_xml_declaration_bom_and_current_extra_nodes() {
|
|
let xml = "\u{feff}<?xml version=\"1.0\" encoding=\"utf-8\"?><service><id>ProxyWardenSingBox</id><name>ProxyWarden Local sing-box</name><executable>%BASE%\\sing-box.exe</executable><arguments>run -c \"%BASE%\\config.json\"</arguments><log mode=\"roll-by-size\"><keepFiles>4</keepFiles></log><onfailure action=\"restart\" /></service>".to_string();
|
|
let host = current_singbox_host_with_xml(&xml);
|
|
|
|
assert!(detect_singbox_install_with_host(&host).is_some());
|
|
}
|
|
|
|
#[test]
|
|
fn portable_singbox_env_override_remains_foreign() {
|
|
let host = MockHost::new()
|
|
.with_env("PROXYWARDEN_SINGBOX_ROOT", r"D:\Portable\sing-box")
|
|
.with_path(r"D:\Portable\sing-box\sing-box.exe");
|
|
|
|
assert!(detect_singbox_install_with_host(&host).is_none());
|
|
assert_eq!(
|
|
inventory_singbox_with_host(&host).classification(),
|
|
ComponentClassification::Foreign
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn missing_local_singbox_returns_optional_install_action_status() {
|
|
let component = singbox_component_from_detection(None);
|
|
|
|
assert_eq!(component.state, ComponentState::Missing);
|
|
assert!(!component.installed);
|
|
assert!(!component.running);
|
|
assert_eq!(component.actions, vec!["Установить Local sing-box"]);
|
|
assert!(component.problems.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn parses_service_pathname_without_accepting_malformed_quotes() {
|
|
assert_eq!(
|
|
service_executable_from_path_name(
|
|
r#""C:\Program Files\ProxyWarden\components\sing-box\ProxyWardenSingBox.exe" install"#,
|
|
),
|
|
Some(PathBuf::from(
|
|
r"C:\Program Files\ProxyWarden\components\sing-box\ProxyWardenSingBox.exe"
|
|
))
|
|
);
|
|
assert_eq!(
|
|
service_executable_from_path_name(r"C:\Tools\ProxiFyre\ProxiFyre.exe --service"),
|
|
Some(PathBuf::from(r"C:\Tools\ProxiFyre\ProxiFyre.exe"))
|
|
);
|
|
assert!(service_executable_from_path_name(r#""C:\Broken\ProxiFyre.exe --service"#).is_none());
|
|
assert!(service_executable_from_path_name(" ").is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn frozen_proxifyre_manifest_matches_all_ten_files_and_nothing_less() {
|
|
let observed = LEGACY_PROXIFYRE_2_2_1_MANIFEST
|
|
.iter()
|
|
.rev()
|
|
.map(|file| LegacyPackageFileIdentity {
|
|
relative_path: PathBuf::from(file.relative_path.to_ascii_uppercase()),
|
|
size: file.size,
|
|
sha256: file.sha256.to_ascii_uppercase(),
|
|
})
|
|
.collect::<Vec<_>>();
|
|
assert!(matches_legacy_proxifyre_2_2_1_manifest(&observed));
|
|
|
|
let mut missing = observed.clone();
|
|
missing.pop();
|
|
assert!(!matches_legacy_proxifyre_2_2_1_manifest(&missing));
|
|
|
|
let mut wrong_size = observed.clone();
|
|
wrong_size[0].size += 1;
|
|
assert!(!matches_legacy_proxifyre_2_2_1_manifest(&wrong_size));
|
|
|
|
let mut wrong_hash = observed.clone();
|
|
wrong_hash[0].sha256 = "0".repeat(64);
|
|
assert!(!matches_legacy_proxifyre_2_2_1_manifest(&wrong_hash));
|
|
|
|
let mut extra = observed.clone();
|
|
extra.push(LegacyPackageFileIdentity {
|
|
relative_path: PathBuf::from("unexpected.dll"),
|
|
size: 1,
|
|
sha256: "0".repeat(64),
|
|
});
|
|
assert!(!matches_legacy_proxifyre_2_2_1_manifest(&extra));
|
|
|
|
let mut duplicate = observed;
|
|
duplicate[0] = duplicate[1].clone();
|
|
assert!(!matches_legacy_proxifyre_2_2_1_manifest(&duplicate));
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(windows)]
|
|
fn reads_windows_pe_file_version_without_executing_binary() {
|
|
let windows_dir = std::env::var("WINDIR").expect("WINDIR on Windows");
|
|
let notepad = PathBuf::from(windows_dir)
|
|
.join("System32")
|
|
.join("notepad.exe");
|
|
let version = SystemProxyfierDetectionHost
|
|
.file_version(¬epad)
|
|
.expect("notepad PE version");
|
|
|
|
assert_eq!(version.split('.').count(), 4);
|
|
assert!(version
|
|
.split('.')
|
|
.all(|segment| segment.parse::<u32>().is_ok()));
|
|
}
|
|
|
|
#[test]
|
|
fn production_component_detection_has_only_native_windows_owners() {
|
|
let source = include_str!("../src/component_detection.rs");
|
|
let source_lower = source.to_ascii_lowercase();
|
|
|
|
for forbidden in [
|
|
"command_no_window(",
|
|
"get-process",
|
|
"get-service",
|
|
"get-ciminstance",
|
|
"\"powershell\"",
|
|
"std::process::command",
|
|
"extern \"system\"",
|
|
"#[link(",
|
|
] {
|
|
assert!(
|
|
!source_lower.contains(forbidden),
|
|
"production detection still contains shell boundary: {forbidden}"
|
|
);
|
|
}
|
|
for native_owner in [
|
|
"CreateToolhelp32Snapshot",
|
|
"OpenSCManagerW",
|
|
"QueryServiceStatusEx",
|
|
"QueryServiceConfigW",
|
|
"winreg::",
|
|
] {
|
|
assert!(
|
|
source.contains(native_owner),
|
|
"native detection owner is missing: {native_owner}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(windows)]
|
|
fn native_process_inventory_finds_the_running_test_binary() {
|
|
let executable_name = std::env::current_exe()
|
|
.expect("current test executable")
|
|
.file_name()
|
|
.expect("current test executable file name")
|
|
.to_string_lossy()
|
|
.into_owned();
|
|
|
|
assert!(SystemProxyfierDetectionHost.process_running(&executable_name));
|
|
assert!(SystemProxyfierDetectionHost.process_running(&executable_name.to_ascii_uppercase()));
|
|
assert!(SystemProxyfierDetectionHost.process_running(executable_name.trim_end_matches(".exe")));
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(windows)]
|
|
fn native_service_inventory_reads_status_and_path_from_scm() {
|
|
let service = SystemProxyfierDetectionHost
|
|
.service_info("EventLog")
|
|
.expect("Windows EventLog service should be queryable without elevation");
|
|
|
|
assert_eq!(service.name, "EventLog");
|
|
assert!(matches!(
|
|
service.status.as_str(),
|
|
"stopped"
|
|
| "start pending"
|
|
| "stop pending"
|
|
| "running"
|
|
| "continue pending"
|
|
| "pause pending"
|
|
| "paused"
|
|
| "unknown"
|
|
));
|
|
assert!(service
|
|
.path_name
|
|
.as_deref()
|
|
.is_some_and(|path| !path.trim().is_empty()));
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct MockHost {
|
|
env: HashMap<String, String>,
|
|
paths: HashSet<String>,
|
|
processes: HashSet<String>,
|
|
services: HashMap<String, String>,
|
|
service_paths: HashMap<String, String>,
|
|
texts: HashMap<String, String>,
|
|
known_binaries: HashSet<String>,
|
|
versions: HashMap<String, String>,
|
|
registry: Vec<RegistryInstallEntry>,
|
|
}
|
|
|
|
impl MockHost {
|
|
fn new() -> Self {
|
|
Self::default()
|
|
}
|
|
|
|
fn with_env(mut self, name: &str, value: &str) -> Self {
|
|
self.env.insert(name.to_string(), value.to_string());
|
|
self
|
|
}
|
|
|
|
fn with_path(mut self, path: &str) -> Self {
|
|
self.paths.insert(normalize_path(path));
|
|
self
|
|
}
|
|
|
|
fn with_process(mut self, process: &str) -> Self {
|
|
self.processes.insert(process.to_ascii_lowercase());
|
|
self
|
|
}
|
|
|
|
fn with_service(mut self, service: &str) -> Self {
|
|
self.services
|
|
.insert(service.to_ascii_lowercase(), "running".to_string());
|
|
self
|
|
}
|
|
|
|
fn with_service_path(mut self, service: &str, path_name: &str) -> Self {
|
|
self.services
|
|
.insert(service.to_ascii_lowercase(), "running".to_string());
|
|
self.service_paths
|
|
.insert(service.to_ascii_lowercase(), path_name.to_string());
|
|
self
|
|
}
|
|
|
|
fn with_stopped_service_path(mut self, service: &str, path_name: &str) -> Self {
|
|
self.services
|
|
.insert(service.to_ascii_lowercase(), "stopped".to_string());
|
|
self.service_paths
|
|
.insert(service.to_ascii_lowercase(), path_name.to_string());
|
|
self
|
|
}
|
|
|
|
fn with_registry(mut self, display_name: &str, install_location: &str) -> Self {
|
|
self.registry.push(RegistryInstallEntry {
|
|
display_name: display_name.to_string(),
|
|
install_location: Some(PathBuf::from(install_location)),
|
|
display_icon: None,
|
|
});
|
|
self
|
|
}
|
|
|
|
fn with_text(mut self, path: &str, contents: &str) -> Self {
|
|
self.paths.insert(normalize_path(path));
|
|
self.texts
|
|
.insert(normalize_path(path), contents.to_string());
|
|
self
|
|
}
|
|
|
|
fn with_known_binary(mut self, path: &str) -> Self {
|
|
self.known_binaries.insert(normalize_path(path));
|
|
self
|
|
}
|
|
|
|
fn with_version(mut self, path: &str, version: &str) -> Self {
|
|
self.versions
|
|
.insert(normalize_path(path), version.to_string());
|
|
self
|
|
}
|
|
}
|
|
|
|
impl ProxyfierDetectionHost for MockHost {
|
|
fn env_var(&self, name: &str) -> Option<String> {
|
|
self.env.get(name).cloned()
|
|
}
|
|
|
|
fn path_exists(&self, path: &Path) -> bool {
|
|
self.paths
|
|
.contains(&normalize_path(&path.display().to_string()))
|
|
}
|
|
|
|
fn process_running(&self, process_name: &str) -> bool {
|
|
self.processes.contains(&process_name.to_ascii_lowercase())
|
|
}
|
|
|
|
fn service_status(&self, service_name: &str) -> Option<String> {
|
|
self.services
|
|
.get(&service_name.to_ascii_lowercase())
|
|
.cloned()
|
|
}
|
|
|
|
fn service_info(
|
|
&self,
|
|
service_name: &str,
|
|
) -> Option<proxywarden_lib::component_detection::DetectedService> {
|
|
let key = service_name.to_ascii_lowercase();
|
|
self.services.get(&key).map(|status| {
|
|
proxywarden_lib::component_detection::DetectedService {
|
|
name: service_name.to_string(),
|
|
status: status.clone(),
|
|
path_name: self.service_paths.get(&key).cloned(),
|
|
}
|
|
})
|
|
}
|
|
|
|
fn registry_install_entries(&self) -> Vec<RegistryInstallEntry> {
|
|
self.registry.clone()
|
|
}
|
|
|
|
fn read_text(&self, path: &Path) -> Option<String> {
|
|
self.texts
|
|
.get(&normalize_path(&path.display().to_string()))
|
|
.cloned()
|
|
}
|
|
|
|
fn file_version(&self, path: &Path) -> Option<String> {
|
|
self.versions
|
|
.get(&normalize_path(&path.display().to_string()))
|
|
.cloned()
|
|
}
|
|
|
|
fn binary_identity(
|
|
&self,
|
|
_component_id: &proxywarden_lib::models::ComponentId,
|
|
path: &Path,
|
|
) -> BinaryIdentityEvidence {
|
|
if self
|
|
.known_binaries
|
|
.contains(&normalize_path(&path.display().to_string()))
|
|
{
|
|
BinaryIdentityEvidence::KnownPackage
|
|
} else {
|
|
BinaryIdentityEvidence::Unknown
|
|
}
|
|
}
|
|
}
|
|
|
|
fn normalize_path(path: &str) -> String {
|
|
path.replace('/', "\\").to_ascii_lowercase()
|
|
}
|
|
|
|
fn winsw_xml() -> &'static str {
|
|
r#"<service>
|
|
<id>ProxyWardenSingBox</id>
|
|
<executable>%BASE%\sing-box.exe</executable>
|
|
<arguments>run -c "%BASE%\config.json"</arguments>
|
|
</service>"#
|
|
}
|
|
|
|
#[test]
|
|
fn current_singbox_recognizes_the_native_installer_xml() {
|
|
let xml = proxywarden_lib::singbox_service::singbox_service_xml();
|
|
for xml in [
|
|
xml.to_owned(),
|
|
xml.replace(""", """),
|
|
xml.replace(""", """),
|
|
] {
|
|
let inventory = inventory_singbox_with_host(¤t_singbox_host_with_xml(&xml));
|
|
assert_eq!(
|
|
inventory.classification(),
|
|
ComponentClassification::ManagedCurrent
|
|
);
|
|
}
|
|
for entity in ["&unknown;", "&quot;", "�", "�"] {
|
|
let xml = xml.replace(""", entity);
|
|
let inventory = inventory_singbox_with_host(¤t_singbox_host_with_xml(&xml));
|
|
assert_ne!(
|
|
inventory.classification(),
|
|
ComponentClassification::ManagedCurrent
|
|
);
|
|
}
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
#[test]
|
|
#[ignore = "read-only smoke test requiring an installed managed sing-box"]
|
|
fn installed_singbox_inventory_is_current() {
|
|
let inventory = proxywarden_lib::component_detection::inventory_singbox();
|
|
assert_eq!(
|
|
inventory.classification(),
|
|
ComponentClassification::ManagedCurrent,
|
|
"{inventory:?}"
|
|
);
|
|
}
|
|
|
|
fn current_singbox_host_with_xml(xml: &str) -> MockHost {
|
|
MockHost::new()
|
|
.with_path(r"C:\Program Files\ProxyWarden\components\sing-box\sing-box.exe")
|
|
.with_path(r"C:\Program Files\ProxyWarden\components\sing-box\ProxyWardenSingBox.exe")
|
|
.with_text(
|
|
r"C:\Program Files\ProxyWarden\components\sing-box\ProxyWardenSingBox.xml",
|
|
xml,
|
|
)
|
|
.with_service_path(
|
|
"ProxyWardenSingBox",
|
|
r#""C:\Program Files\ProxyWarden\components\sing-box\ProxyWardenSingBox.exe""#,
|
|
)
|
|
}
|