diff --git a/CHANGELOG.md b/CHANGELOG.md index 01a40ae..fe45581 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,22 @@ All notable changes are documented here. Versions follow Semantic Versioning. No unreleased changes. +## [1.3.0] - 2026-08-20 + +### Added + +- A read-only `doctor` command for platform, .NET Framework, WMI, native-tool, and Defender capability checks. +- `doctor --json` output with a versioned document schema for automation and issue reports. +- A Windows validation matrix documenting what CI and local smoke tests do and do not prove. + +### Compatibility + +- Restore accepts v1.2.0 and v1.2.1 backup manifests when their machine, hash, schema, and allowlist checks pass. + +### Verification + +- Windows build, CMD smoke tests, `doctor`, `doctor --json`, and self-test passed locally and in GitHub Actions. + ## [1.2.1] - 2026-08-20 ### Fixed @@ -39,7 +55,8 @@ Build, launch, version, self-test, audit, plan, report generation, listener list The first public Windows toolkit layout and safety documentation. -[Unreleased]: https://github.com/KBT096/windows-secure-toolkit/compare/v1.2.1...HEAD +[Unreleased]: https://github.com/KBT096/windows-secure-toolkit/compare/v1.3.0...HEAD +[1.3.0]: https://github.com/KBT096/windows-secure-toolkit/releases/tag/v1.3.0 [1.2.1]: https://github.com/KBT096/windows-secure-toolkit/releases/tag/v1.2.1 [1.2.0]: https://github.com/KBT096/windows-secure-toolkit/releases/tag/v1.2.0 [0.1.0]: https://github.com/KBT096/windows-secure-toolkit/releases/tag/v0.1.0 diff --git a/PROJECT_SUMMARY.md b/PROJECT_SUMMARY.md index 90cce8c..d5aff8c 100644 --- a/PROJECT_SUMMARY.md +++ b/PROJECT_SUMMARY.md @@ -22,6 +22,7 @@ - 注册表、Guest、SMBv1、RDP/NLA 和防火墙策略的备份与恢复; - 保守基线的预览、确认、执行和逐项结果; - Defender 扫描、DISM/SFC 验证、监听端口与版本检查; +- 只读 `doctor` 兼容性诊断及其 JSON 输出; - 交互菜单、命令行参数和无修改自检。 `src/WindowsSecureToolkit.csproj` 目标为 .NET Framework 4.8,避免给支持的 Windows 系统再塞一个常驻运行时。 @@ -49,6 +50,7 @@ - 真实 `cmd.exe` 入口; - 版本、帮助和计划模式; - 审计报告生成与 JSON 文件; +- `doctor` 人类可读和 `--json` 两种输出; - 缺少恢复路径时的错误码; - 不存在旧脚本核心和动态远程执行模式。 @@ -65,6 +67,7 @@ ## 6. 后续方向 - 扩展真实 Windows Server 版本测试矩阵; +- 让 `doctor` 输出成为问题报告前的统一环境快照; - 为审计 JSON 提供稳定 schema 与兼容性测试; - 增加由用户明确选择的单项配置,而不是扩大默认基线; - 根据真实 Issue、PR 和运行反馈调整检查项; diff --git a/README.en.md b/README.en.md index a245afc..c23cc43 100644 --- a/README.en.md +++ b/README.en.md @@ -6,11 +6,11 @@ Windows security settings are a bit like the fuse box: nobody wants to stare at it all day, but a small record is useful when something goes wrong. -Current version: `1.2.1`. +Current version: `1.3.0`. This is a small local toolkit with a CMD/BAT entry point and a C# engine. It checks, previews, backs up, and restores a conservative set of settings. It does not promise a magic “secure” button. Sadly, those are still out of stock. -## What it does +## Core architecture and features - read-only checks for firewall, Defender, UAC, SMBv1, Guest, RDP/NLA, AutoRun, updates, and pending reboot; - Markdown and JSON audit reports; @@ -18,9 +18,10 @@ This is a small local toolkit with a CMD/BAT entry point and a C# engine. It che - a SHA-256 checked manifest and firewall backup before changes; - same-machine, allowlisted restore; - Defender quick scan, DISM/SFC verification, and TCP listener listing; +- a read-only compatibility doctor for Windows, .NET Framework, WMI, and native tools, with JSON output; - GitHub Release metadata checks only. It does not download and run remote code. -## What it does not do +## Notes and boundaries - open inbound ports, enable RDP, or create administrator accounts; - reboot the computer or automatically repair DISM/SFC findings; @@ -28,17 +29,18 @@ This is a small local toolkit with a CMD/BAT entry point and a C# engine. It che - bypass organization policy or endpoint management; - turn static checks into claims about every Windows edition. -## Quick start +## 📖📖 Quick entry: how do I configure and run Windows Secure Toolkit? The target is Windows 10/11 and Windows Server 2019/2022/2025. A compiled build needs .NET Framework 4.8; building from source needs the .NET 6 SDK or newer. -No SDK? Grab `windows-secure-toolkit-v1.2.1-win-x64.zip` from the [v1.2.1 Release](https://github.com/KBT096/windows-secure-toolkit/releases/tag/v1.2.1), extract it, and run the entry point. +No SDK? Grab `windows-secure-toolkit-v1.3.0-win-x64.zip` from the [v1.3.0 Release](https://github.com/KBT096/windows-secure-toolkit/releases/tag/v1.3.0), extract it, and run the entry point. ```cmd build.cmd win_secure.cmd self-test win_secure.cmd audit win_secure.cmd plan +win_secure.cmd doctor win_secure.cmd apply ``` @@ -60,6 +62,7 @@ win_secure.cmd restore "C:\ProgramData\WindowsSecureToolkit\Backups\20260818-120 | `win_secure.cmd scan` | Defender quick scan | | `win_secure.cmd verify` | DISM/SFC read-only verification | | `win_secure.cmd ports` | Show TCP listeners | +| `win_secure.cmd doctor [--json]` | Read-only compatibility and dependency checks | | `win_secure.cmd update` | Check the latest Release | | `win_secure.cmd version` | Print the version | | `win_secure.cmd self-test` | Run the no-change self-test | @@ -72,13 +75,20 @@ win_secure.cmd restore "C:\ProgramData\WindowsSecureToolkit\Backups\20260818-120 - `win_secure.cmd` and `win_secure.bat` - user entry points; - `scripts/Test-Repository.cmd` - build and smoke-test gate; - `docs/THREAT_MODEL.md` - boundaries and threat model. +- `docs/WINDOWS_VALIDATION.md` - doctor command and Windows validation matrix. ## Verification note Windows 11 Pro for Workstations build 26200 has been used for build, launch, version, self-test, audit, plan, reports, listener listing, and Release checks. Public GitHub Actions also builds and runs the smoke tests on Windows. +`doctor` is a read-only capability probe. Missing optional components are reported as `Unavailable`, not silently changed. `doctor --json` emits a machine-readable document with a `SchemaVersion`, which is useful before collecting logs. Windows editions, organization policy, and third-party security software can legitimately produce different results. + System-changing Apply/Restore has not been run on the maintainer machine. The release notes say so plainly. Read the plan, keep the backup, and do not expect Windows to clap when you click the button. +## Copyright and component notice + +The code in this repository is released under the [MIT License](LICENSE). The build targets .NET Framework 4.8 and the runtime uses Windows components such as `netsh`, `dism`, `sfc`, `netstat`, WMI, and Microsoft Defender; their licensing and use remain subject to the applicable Microsoft Windows terms. CI uses GitHub Actions with a pinned `actions/checkout` revision for repository validation only. This repository does not bundle YABS, NextTrace, or other VPS probe components. + ## Contributing and license See [CONTRIBUTING.md](CONTRIBUTING.md) and [SECURITY.md](SECURITY.md). Ordinary questions belong in [Issues](https://github.com/KBT096/windows-secure-toolkit/issues). diff --git a/README.md b/README.md index 8a85a35..ba76c30 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Windows 的安全设置有点像家里的电箱:平时没人想看,真出问题又希望它有记录。 -当前版本:`1.2.1`。 +当前版本:`1.3.0`。 这个小工具从 CMD/BAT 进去,用 C# 做检查、预览、备份和恢复。它不负责把电脑变成“绝对安全”,只负责把常见的几件事做得清楚一点。 @@ -18,6 +18,7 @@ Windows 的安全设置有点像家里的电箱:平时没人想看,真出问 - 应用前保存清单、SHA-256 和防火墙策略; - 在同一台电脑上校验后恢复备份; - Defender 快速扫描、DISM/SFC 只读检查、TCP 监听端口查看; +- 本机兼容性诊断:平台、.NET Framework、WMI 和原生命令能力,支持 JSON 输出; - 只查询 GitHub Release 版本,不下载脚本,更不会下载完就“相信它”。 ## 注意事项 @@ -34,13 +35,14 @@ Windows 的安全设置有点像家里的电箱:平时没人想看,真出问 支持 Windows 10/11 和 Windows Server 2019/2022/2025。运行已编译版本只需要 .NET Framework 4.8;从源码构建需要 .NET 6 SDK 或更高版本。 -不想装 SDK?可以从 [v1.2.1 Release](https://github.com/KBT096/windows-secure-toolkit/releases/tag/v1.2.1) 下载 `windows-secure-toolkit-v1.2.1-win-x64.zip`,解压后直接运行入口文件。 +不想装 SDK?可以从 [v1.3.0 Release](https://github.com/KBT096/windows-secure-toolkit/releases/tag/v1.3.0) 下载 `windows-secure-toolkit-v1.3.0-win-x64.zip`,解压后直接运行入口文件。 ```cmd build.cmd win_secure.cmd self-test win_secure.cmd audit win_secure.cmd plan +win_secure.cmd doctor ``` 第一次使用建议只读审计,然后看计划: @@ -74,6 +76,7 @@ win_secure.cmd restore "C:\ProgramData\WindowsSecureToolkit\Backups\20260818-120 | `win_secure.cmd scan` | Defender 快速扫描 | | `win_secure.cmd verify` | DISM/SFC 只读验证 | | `win_secure.cmd ports` | TCP 监听端口 | +| `win_secure.cmd doctor [--json]` | 只读检查本机兼容性与依赖能力 | | `win_secure.cmd update` | 查询最新 Release | | `win_secure.cmd version` | 输出版本号 | | `win_secure.cmd self-test` | 无修改自检 | @@ -88,11 +91,14 @@ win_secure.cmd restore "C:\ProgramData\WindowsSecureToolkit\Backups\20260818-120 - `win_secure.cmd` / `win_secure.bat`:用户入口; - `scripts/Test-Repository.cmd`:构建、入口和报告烟雾测试; - `docs/THREAT_MODEL.md`:边界和威胁模型。 +- `docs/WINDOWS_VALIDATION.md`:诊断命令与 Windows 验证矩阵。 ## 验证范围 本机 Windows 11 专业工作站版 build 26200 已验证:C# 构建、CMD/BAT 启动、版本、自检、审计、计划、报告生成、端口查看和 Release 检查。公开 GitHub Actions 也会在 Windows runner 上构建并运行烟雾测试。 +`doctor` 是只读能力探测,不会因为缺少可选组件就修改系统;`doctor --json` 输出带 `SchemaVersion` 的机器可读结果,适合在收集日志前先确认环境。不同 Windows 版本、组织策略和第三方防护软件可能使某些项目显示为 `Unavailable`,这代表需要人工复核,不代表工具已经替你修复。 + 实际修改系统的 Apply/Restore 流程没有在维护者机器上执行,因此发布说明不会把它写成已经覆盖所有环境。请先看计划,备份也别删,电脑通常不会因为你多看一眼就生气。 ## 参与和报告问题 diff --git a/docs/WINDOWS_VALIDATION.md b/docs/WINDOWS_VALIDATION.md new file mode 100644 index 0000000..9161dd6 --- /dev/null +++ b/docs/WINDOWS_VALIDATION.md @@ -0,0 +1,27 @@ +# Windows validation notes + +This document describes the small, repeatable validation surface behind the `doctor` command. It is deliberately narrower than a claim that every Windows edition has been tested. + +## Read-only doctor + +```cmd +win_secure.cmd doctor +win_secure.cmd doctor --json +``` + +The command reads platform information, the .NET Framework 4.x `Release` registry value, WMI operating-system metadata, the native tools used by this repository, and the optional Defender command-line tool. It does not change registry values, firewall policy, services, accounts, or files. `Unavailable` means that the capability could not be confirmed in the current environment; it is not an instruction to install or enable anything. + +The JSON form has `SchemaVersion`, `ToolkitVersion`, `GeneratedUtc`, `ComputerName`, and a `Checks` array. Each check has an identifier, status, human-readable summary, detail, and a `Required` hint. Consumers should treat unknown statuses or fields as review items so the schema can grow without breaking older collectors. + +## Current matrix + +| Surface | Local Windows 11 Pro for Workstations build 26200 | GitHub Actions Windows runner | What it proves | +| --- | --- | --- | --- | +| C# build | Verified | Verified | The checked-in source compiles for .NET Framework 4.8 | +| CMD/BAT entry points | Verified | Verified | The real Windows entry point forwards exit codes | +| `self-test`, `version`, `help`, `plan` | Verified | Verified | Core parsing and no-change checks run | +| `doctor` and `doctor --json` | Verified | Verified | Diagnostic output is available without system changes | +| Audit reports | Verified | Verified | Markdown and JSON reports can be generated | +| Apply / Restore | Not run on the maintainer machine | Not run | Static coverage does not replace a deliberate, backed-up system test | + +The CI job is a smoke-test gate, not a privileged deployment test. Before reporting a real Apply or Restore result, record the Windows edition/build, elevation state, exact command, backup path, and whether a reboot or policy refresh occurred. diff --git a/scripts/Test-Repository.cmd b/scripts/Test-Repository.cmd index 64d4500..02a2dd5 100644 --- a/scripts/Test-Repository.cmd +++ b/scripts/Test-Repository.cmd @@ -22,6 +22,7 @@ call :require_file "%TOOL_ROOT%README.md" call :require_file "%TOOL_ROOT%LICENSE" call :require_file "%TOOL_ROOT%SECURITY.md" call :require_file "%TOOL_ROOT%docs\THREAT_MODEL.md" +call :require_file "%TOOL_ROOT%docs\WINDOWS_VALIDATION.md" findstr /s /i /n "Invoke-Expression certutil -decode" "%TOOL_ROOT%src\*.cs" "%TOOL_ROOT%*.cmd" >nul 2>nul if not errorlevel 1 ( @@ -42,6 +43,8 @@ call :run_ok self-test call :run_ok version call :run_ok help call :run_ok plan +call :run_ok doctor +call :run_ok_args doctor --json if exist "%TEST_ROOT%" rmdir /s /q "%TEST_ROOT%" mkdir "%TEST_ROOT%" >nul 2>nul @@ -104,3 +107,14 @@ if errorlevel 1 ( echo [完成] 命令通过:%~1 ) exit /b 0 + +:run_ok_args +set /a CHECKS+=1 +call "%TOOL_ROOT%win_secure.cmd" %~1 %~2 >nul +if errorlevel 1 ( + echo [失败] 命令失败:%~1 %~2 + set /a FAILURES+=1 +) else ( + echo [完成] 命令通过:%~1 %~2 +) +exit /b 0 diff --git a/src/WinSecure.cs b/src/WinSecure.cs index b50d988..27fcee9 100644 --- a/src/WinSecure.cs +++ b/src/WinSecure.cs @@ -21,9 +21,15 @@ namespace WindowsSecureToolkit { internal static class Program { - private const string Version = "1.2.1"; + private const string Version = "1.3.0"; private const string ToolkitName = "Windows Secure Toolkit"; private const string ReleaseApiUrl = "https://api.github.com/repos/KBT096/windows-secure-toolkit/releases/latest"; + private static readonly HashSet SupportedBackupVersions = new HashSet(StringComparer.OrdinalIgnoreCase) + { + Version, + "1.2.0", + "1.2.1" + }; private static readonly string[] RegistryAllowlist = { "HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Policies\\System\\EnableLUA", @@ -78,6 +84,7 @@ private static int Main(string[] args) case "scan": return RunDefenderScan(); case "verify": return RunSystemVerify(); case "ports": return ShowListeningPorts(); + case "doctor": return RunDoctor(options.JsonOutput); case "update": return CheckForUpdate(); case "version": Console.WriteLine(Version); return 0; case "self-test": return RunSelfTest(); @@ -131,6 +138,7 @@ private static void ShowHelp() Console.WriteLine(" scan 运行 Defender 快速扫描"); Console.WriteLine(" verify 运行 DISM/SFC 只读验证"); Console.WriteLine(" ports 查看 TCP 监听端口"); + Console.WriteLine(" doctor [--json] 运行本机兼容性诊断(只读)"); Console.WriteLine(" update 查询 GitHub 最新 Release"); Console.WriteLine(" version 输出版本号"); Console.WriteLine(" self-test 运行无修改自检"); @@ -151,6 +159,7 @@ private static int ShowMenu() Console.WriteLine("7. 查看监听端口"); Console.WriteLine("8. 检查版本"); Console.WriteLine("9. 自检"); + Console.WriteLine("10. 本机兼容性诊断(只读)"); Console.WriteLine("0. 退出"); Console.Write("请选择:"); string choice = Console.ReadLine(); @@ -167,6 +176,7 @@ private static int ShowMenu() case "7": return ShowListeningPorts(); case "8": return CheckForUpdate(); case "9": return RunSelfTest(); + case "10": return RunDoctor(false); case "0": return 0; default: Message("错误", "没有这个选项。"); return 2; } @@ -180,6 +190,7 @@ private sealed class CliOptions public bool Yes; public bool DryRun; public bool NoColor; + public bool JsonOutput; public static CliOptions Parse(string[] args) { @@ -204,6 +215,7 @@ public static CliOptions Parse(string[] args) if (arg.Equals("--yes", StringComparison.OrdinalIgnoreCase) || arg.Equals("-Yes", StringComparison.OrdinalIgnoreCase)) result.Yes = true; else if (arg.Equals("--dry-run", StringComparison.OrdinalIgnoreCase) || arg.Equals("-DryRun", StringComparison.OrdinalIgnoreCase)) result.DryRun = true; else if (arg.Equals("--no-color", StringComparison.OrdinalIgnoreCase) || arg.Equals("-NoColor", StringComparison.OrdinalIgnoreCase)) result.NoColor = true; + else if (arg.Equals("--json", StringComparison.OrdinalIgnoreCase) || arg.Equals("-Json", StringComparison.OrdinalIgnoreCase)) result.JsonOutput = true; else if (arg.Equals("--backup-path", StringComparison.OrdinalIgnoreCase) || arg.Equals("-BackupPath", StringComparison.OrdinalIgnoreCase)) { if (index >= args.Length) throw new ArgumentException(arg + " 缺少路径。"); @@ -231,7 +243,7 @@ private static string NormalizeAction(string value) if (action == "listeningports") return "ports"; if (action == "updatecheck") return "update"; if (action == "selftest") return "self-test"; - string[] known = { "menu", "audit", "apply", "plan", "restore", "scan", "verify", "ports", "update", "version", "self-test", "help" }; + string[] known = { "menu", "audit", "apply", "plan", "restore", "scan", "verify", "ports", "doctor", "update", "version", "self-test", "help" }; if (!known.Contains(action)) throw new ArgumentException("未知命令:" + value); return action; } @@ -815,7 +827,7 @@ private static BaselineSnapshot ReadAndValidateManifest(string manifestPath) BaselineSnapshot snapshot = Json.Deserialize(File.ReadAllText(manifestPath, Encoding.UTF8)); if (snapshot == null || snapshot.SchemaVersion != 1) throw new InvalidOperationException("不支持的备份格式版本。"); - if (!snapshot.ToolkitVersion.Equals(Version, StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException("备份版本与当前程序不匹配:" + snapshot.ToolkitVersion); + if (!IsSupportedBackupVersion(snapshot.ToolkitVersion)) throw new InvalidOperationException("备份版本与当前程序不兼容:" + snapshot.ToolkitVersion); if (!Environment.MachineName.Equals(snapshot.ComputerName, StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException("备份不是由当前计算机生成的。"); if (!string.Equals(snapshot.FirewallExportFile, "firewall.wfw", StringComparison.Ordinal)) throw new InvalidOperationException("防火墙导出文件名不在白名单内。"); string firewallPath = Path.Combine(directory, snapshot.FirewallExportFile); @@ -831,6 +843,11 @@ private static BaselineSnapshot ReadAndValidateManifest(string manifestPath) return snapshot; } + private static bool IsSupportedBackupVersion(string toolkitVersion) + { + return !string.IsNullOrWhiteSpace(toolkitVersion) && SupportedBackupVersions.Contains(toolkitVersion); + } + private static void ValidateRegistryAllowlist(IEnumerable values) { var allowed = new HashSet(RegistryAllowlist, StringComparer.OrdinalIgnoreCase); @@ -1062,6 +1079,188 @@ private static string GetOsDescription() return "Windows(版本信息不可用)"; } + private sealed class DoctorCheck + { + public string Id; + public string Status; + public string Summary; + public string Detail; + public bool Required; + } + + private sealed class DoctorDocument + { + public int SchemaVersion = 1; + public string ToolkitVersion = Version; + public string GeneratedUtc; + public string ComputerName; + public List Checks; + } + + private static int RunDoctor(bool jsonOutput) + { + List checks = CollectDoctorChecks(); + int failures = checks.Count(check => check.Status == "Fail"); + if (jsonOutput) + { + Console.WriteLine(Json.Serialize(new DoctorDocument + { + GeneratedUtc = DateTime.UtcNow.ToString("o"), + ComputerName = Environment.MachineName, + Checks = checks + })); + return failures == 0 ? 0 : 4; + } + + Section("本机兼容性诊断(只读)"); + Console.WriteLine("检查项 状态 说明"); + Console.WriteLine("------------------------------------------------------------"); + foreach (DoctorCheck check in checks) + { + Console.WriteLine("{0,-19} {1,-11} {2}", check.Id, check.Status, check.Summary); + if (!string.IsNullOrWhiteSpace(check.Detail)) Console.WriteLine(" {0}", check.Detail); + } + int unavailable = checks.Count(check => check.Status == "Unavailable"); + if (failures > 0) + { + Message("错误", "诊断发现 " + failures + " 个阻断项;没有修改系统。"); + return 4; + } + Message(unavailable > 0 ? "警告" : "完成", "诊断完成:" + checks.Count + " 项检查,没有修改系统。" + (unavailable > 0 ? "部分能力需要人工复核。" : string.Empty)); + return 0; + } + + private static List CollectDoctorChecks() + { + var checks = new List(); + bool isWindows = Environment.OSVersion.Platform == PlatformID.Win32NT; + checks.Add(new DoctorCheck + { + Id = "platform", + Status = isWindows ? "Pass" : "Fail", + Summary = isWindows ? "检测到 Windows 平台" : "当前平台不是 Windows", + Detail = Environment.OSVersion.VersionString, + Required = true + }); + + string osDescription = GetOsDescription(); + bool wmiAvailable = !string.IsNullOrWhiteSpace(osDescription) && osDescription.IndexOf("不可用", StringComparison.OrdinalIgnoreCase) < 0; + checks.Add(new DoctorCheck + { + Id = "wmi", + Status = wmiAvailable ? "Pass" : "Unavailable", + Summary = wmiAvailable ? "WMI 可以读取操作系统信息" : "WMI 操作系统信息不可用", + Detail = osDescription, + Required = false + }); + + int? frameworkRelease = ReadDotNetFrameworkRelease(); + checks.Add(new DoctorCheck + { + Id = "dotnet-framework", + Status = frameworkRelease.HasValue && frameworkRelease.Value >= 528040 ? "Pass" : (frameworkRelease.HasValue ? "Fail" : "Unavailable"), + Summary = frameworkRelease.HasValue ? ".NET Framework 4.8 Release key:" + frameworkRelease.Value : "无法读取 .NET Framework 4.x Release key", + Detail = "要求 Release >= 528040(.NET Framework 4.8)", + Required = true + }); + + checks.Add(new DoctorCheck + { + Id = "administrator", + Status = IsAdministrator() ? "Info" : "Review", + Summary = IsAdministrator() ? "当前进程具有管理员权限" : "当前进程不是管理员权限", + Detail = "审计、doctor 和 plan 不需要提权;apply、restore、scan、verify 的部分步骤需要管理员权限。", + Required = false + }); + + AddToolCheck(checks, "netsh", "netsh.exe 可用(防火墙状态与备份)", true); + AddToolCheck(checks, "netstat", "netstat.exe 可用(监听端口)", true); + AddToolCheck(checks, "dism", "dism.exe 可用(SMBv1 与系统验证)", true); + AddToolCheck(checks, "sfc", "sfc.exe 可用(系统文件验证)", true); + + string defenderPath = FindDefenderExecutable(); + checks.Add(new DoctorCheck + { + Id = "defender", + Status = string.IsNullOrWhiteSpace(defenderPath) ? "Unavailable" : "Pass", + Summary = string.IsNullOrWhiteSpace(defenderPath) ? "未找到 MpCmdRun.exe" : "找到 Microsoft Defender 命令行工具", + Detail = string.IsNullOrWhiteSpace(defenderPath) ? "可能由组织策略、版本差异或第三方防护软件导致。" : defenderPath, + Required = false + }); + + checks.Add(new DoctorCheck + { + Id = "release-api", + Status = ReleaseApiUrl.StartsWith("https://api.github.com/", StringComparison.OrdinalIgnoreCase) ? "Pass" : "Fail", + Summary = "Release 查询地址固定为 GitHub HTTPS API", + Detail = ReleaseApiUrl, + Required = false + }); + return checks; + } + + private static void AddToolCheck(List checks, string id, string summary, bool required) + { + string executable = FindExecutable(id + ".exe"); + checks.Add(new DoctorCheck + { + Id = id, + Status = string.IsNullOrWhiteSpace(executable) ? "Unavailable" : "Pass", + Summary = string.IsNullOrWhiteSpace(executable) ? summary.Replace("可用", "未找到") : summary, + Detail = string.IsNullOrWhiteSpace(executable) ? "当前环境没有找到该系统工具;对应命令可能无法运行。" : executable, + Required = required + }); + } + + private static string FindDefenderExecutable() + { + var candidates = new List(); + string programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles); + string programFilesX86 = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86); + if (!string.IsNullOrWhiteSpace(programFiles)) candidates.Add(Path.Combine(programFiles, "Windows Defender", "MpCmdRun.exe")); + if (!string.IsNullOrWhiteSpace(programFilesX86)) candidates.Add(Path.Combine(programFilesX86, "Windows Defender", "MpCmdRun.exe")); + foreach (string candidate in candidates) + { + try { if (File.Exists(candidate)) return candidate; } catch { } + } + return FindExecutable("MpCmdRun.exe"); + } + + private static string FindExecutable(string fileName) + { + if (string.IsNullOrWhiteSpace(fileName)) return null; + var directories = new List(); + string systemRoot = Environment.GetEnvironmentVariable("SystemRoot"); + if (!string.IsNullOrWhiteSpace(systemRoot)) directories.Add(Path.Combine(systemRoot, "System32")); + string path = Environment.GetEnvironmentVariable("PATH"); + if (!string.IsNullOrWhiteSpace(path)) directories.AddRange(path.Split(new[] { Path.PathSeparator }, StringSplitOptions.RemoveEmptyEntries)); + foreach (string directory in directories) + { + try + { + string candidate = Path.Combine(directory.Trim().Trim('"'), fileName); + if (File.Exists(candidate)) return candidate; + } + catch { } + } + return null; + } + + private static int? ReadDotNetFrameworkRelease() + { + try + { + using (RegistryKey key = Registry.LocalMachine.OpenSubKey("SOFTWARE\\Microsoft\\NET Framework Setup\\NDP\\v4\\Full", false)) + { + if (key == null) return null; + object value = key.GetValue("Release", null, RegistryValueOptions.DoNotExpandEnvironmentNames); + int release; + return value != null && int.TryParse(Convert.ToString(value), out release) ? (int?)release : null; + } + } + catch { return null; } + } + private static int RunAudit(string requestedPath) { Section("只读安全审计"); @@ -1276,6 +1475,28 @@ private static int RunSelfTest() } catch (Exception ex) { failures.Add("注册表白名单测试失败:" + ex.Message); } + try + { + List doctorChecks = CollectDoctorChecks(); + string doctorJson = Json.Serialize(new DoctorDocument + { + GeneratedUtc = DateTime.UtcNow.ToString("o"), + ComputerName = Environment.MachineName, + Checks = doctorChecks + }); + DoctorDocument doctorRoundTrip = Json.Deserialize(doctorJson); + if (doctorRoundTrip == null || doctorRoundTrip.SchemaVersion != 1 || doctorRoundTrip.Checks == null || doctorRoundTrip.Checks.Count == 0) + { + failures.Add("doctor JSON 往返测试失败。"); + } + } + catch (Exception ex) { failures.Add("doctor 诊断测试失败:" + ex.Message); } + + if (!IsSupportedBackupVersion("1.2.0") || !IsSupportedBackupVersion("1.2.1") || IsSupportedBackupVersion("0.1.0")) + { + failures.Add("旧版本备份兼容性测试失败。"); + } + string temp = Path.Combine(Path.GetTempPath(), "windows-secure-toolkit-selftest-" + Guid.NewGuid().ToString("N")); try {