Skip to main content

hematite/agent/
fix_recipes.rs

1/// Fix recipe lookup table.
2///
3/// Maps finding patterns from health_report / inspect_host output to
4/// first-line action steps. No model required — this is curated knowledge,
5/// not inference. Covers ~80% of what an IT person sees in a normal day.
6
7pub struct Recipe {
8    pub severity: &'static str, // "ACTION", "INVESTIGATE", "MONITOR"
9    pub title: &'static str,
10    pub steps: &'static [&'static str],
11    pub dig_deeper: Option<&'static str>, // inspect_host topic to run for more detail
12}
13
14/// Match a health_report or inspect_host output line against known recipes.
15/// Returns all recipes that apply, in priority order.
16pub fn match_recipes(output: &str) -> Vec<&'static Recipe> {
17    let lower = output.to_ascii_lowercase();
18    let mut matches: Vec<&'static Recipe> = Vec::new();
19
20    for recipe in ALL_RECIPES {
21        if recipe.triggers.iter().any(|t| lower.contains(t)) {
22            matches.push(&recipe.recipe);
23        }
24    }
25
26    matches
27}
28
29struct RecipeEntry {
30    triggers: &'static [&'static str],
31    recipe: Recipe,
32}
33
34static ALL_RECIPES: &[RecipeEntry] = &[
35    // ── Disk / Storage ────────────────────────────────────────────────────────
36    RecipeEntry {
37        triggers: &["very low", "disk:", "free space"],
38        recipe: Recipe {
39            severity: "ACTION",
40            title: "Low disk space",
41            steps: &[
42                "Open Disk Cleanup: press Win+R → type 'cleanmgr' → select C: → check all boxes including 'Windows Update Cleanup'",
43                "Empty the Recycle Bin: right-click desktop icon → Empty Recycle Bin",
44                "Clear Temp folder: press Win+R → type '%temp%' → Ctrl+A → Delete (skip files in use)",
45                "Check largest folders: open PowerShell → Get-ChildItem C:\\ -Recurse -ErrorAction SilentlyContinue | Sort-Object Length -Descending | Select-Object -First 20 FullName, Length",
46                "If space is still tight, run: winget install -e --id Microsoft.PowerToys then use PowerToys Disk Space Analyzer",
47            ],
48            dig_deeper: Some("storage"),
49        },
50    },
51    RecipeEntry {
52        triggers: &["disk_health", "smart", "predictive failure", "wear"],
53        recipe: Recipe {
54            severity: "ACTION",
55            title: "Drive health warning — possible failure",
56            steps: &[
57                "Back up your important files immediately before doing anything else",
58                "Verify the SMART status: open PowerShell (admin) → Get-PhysicalDisk | Select FriendlyName, HealthStatus, OperationalStatus",
59                "If HealthStatus is 'Unhealthy' or 'Warning', replace the drive — do not wait",
60                "For SSDs: check manufacturer's NVMe/SSD tool (Samsung Magician, Crucial Storage Executive, etc.) for wear level",
61            ],
62            dig_deeper: Some("disk_health"),
63        },
64    },
65
66    // ── Reboot ────────────────────────────────────────────────────────────────
67    RecipeEntry {
68        triggers: &["pending reboot", "restart when convenient", "reboot required"],
69        recipe: Recipe {
70            severity: "INVESTIGATE",
71            title: "Restart required",
72            steps: &[
73                "Save your work and restart the computer — pending file operations and updates cannot apply until you do",
74                "After restarting, run this report again to confirm the reboot flag cleared",
75                "If the flag persists after a restart, check Windows Update: Settings → Windows Update → View update history → look for stuck installs",
76            ],
77            dig_deeper: Some("pending_reboot"),
78        },
79    },
80
81    // ── Event log errors ──────────────────────────────────────────────────────
82    RecipeEntry {
83        triggers: &["critical/error event", "error events in windows event log", "critical error"],
84        recipe: Recipe {
85            severity: "INVESTIGATE",
86            title: "Windows event log errors detected",
87            steps: &[
88                "Find the top error sources: PowerShell → Get-WinEvent -FilterHashtable @{LogName='System','Application';Level=1,2} -MaxEvents 100 | Group-Object ProviderName | Sort-Object Count -Descending | Select -First 10",
89                "One crashing service or driver usually causes most of the noise — focus on the source with the highest count",
90                "For 'Service Control Manager' errors: check which service is crashing → Get-WinEvent -FilterHashtable @{LogName='System';ProviderName='Service Control Manager';Level=2} -MaxEvents 10 | Select Message",
91                "For application crashes: check AppEvent for the faulting app name → Get-WinEvent -FilterHashtable @{LogName='Application';Level=2} -MaxEvents 10 | Select TimeCreated,Message",
92            ],
93            dig_deeper: Some("log_check"),
94        },
95    },
96
97    // ── Services ──────────────────────────────────────────────────────────────
98    RecipeEntry {
99        triggers: &["critical service", "not running: windefend", "not running: eventlog", "not running: dnscache"],
100        recipe: Recipe {
101            severity: "ACTION",
102            title: "Critical Windows service not running",
103            steps: &[
104                "Open Services: press Win+R → type 'services.msc' → Enter",
105                "Find the stopped service, right-click → Start",
106                "If it fails to start, right-click → Properties → Recovery tab → set 'First failure' to 'Restart the Service'",
107                "For Windows Defender (WinDefend) stopped: open Windows Security → Virus & threat protection → turn on Real-time protection",
108                "If EventLog is stopped, restart is required — this service cannot be started manually once stopped",
109            ],
110            dig_deeper: Some("services"),
111        },
112    },
113
114    // ── Network ───────────────────────────────────────────────────────────────
115    RecipeEntry {
116        triggers: &["internet connectivity: unreachable", "could not ping 1.1.1.1"],
117        recipe: Recipe {
118            severity: "ACTION",
119            title: "No internet connectivity",
120            steps: &[
121                "Check physical connection: is the Ethernet cable plugged in, or is Wi-Fi connected?",
122                "Test gateway reachability: PowerShell → Test-Connection (Get-NetRoute -DestinationPrefix '0.0.0.0/0').NextHop -Count 1",
123                "Flush DNS cache: PowerShell (admin) → Clear-DnsClientCache",
124                "Reset TCP/IP stack: PowerShell (admin) → netsh int ip reset; netsh winsock reset → then restart",
125                "If on Wi-Fi: forget the network and reconnect, or try 'netsh wlan disconnect' then 'netsh wlan connect name=\"SSID\"'",
126            ],
127            dig_deeper: Some("connectivity"),
128        },
129    },
130    RecipeEntry {
131        triggers: &["high latency", "ms rtt — high latency"],
132        recipe: Recipe {
133            severity: "MONITOR",
134            title: "High network latency detected",
135            steps: &[
136                "Run a traceroute to find where the delay is: PowerShell → tracert 1.1.1.1",
137                "Check for background bandwidth consumers: Task Manager → Performance → Open Resource Monitor → Network tab",
138                "If on Wi-Fi, check signal strength and try moving closer to the router or switching to 5GHz",
139                "Check your ISP's status page for outages in your area",
140            ],
141            dig_deeper: Some("latency"),
142        },
143    },
144
145    // ── RAM ───────────────────────────────────────────────────────────────────
146    RecipeEntry {
147        triggers: &["ram:", "very low", "running a bit low", "free of"],
148        recipe: Recipe {
149            severity: "MONITOR",
150            title: "High memory usage",
151            steps: &[
152                "Find the top RAM consumers: Task Manager → Memory column (sort descending)",
153                "Close unused browser tabs — each tab can consume 100–500 MB",
154                "Check for memory leaks: if one process is growing over time without release, restart it",
155                "Disable startup programs that aren't needed: Task Manager → Startup tab → disable high-impact items",
156                "If consistently above 85% with normal usage, consider adding RAM",
157            ],
158            dig_deeper: Some("resource_load"),
159        },
160    },
161
162    // ── Thermal ───────────────────────────────────────────────────────────────
163    RecipeEntry {
164        triggers: &["very high", "check cooling", "elevated under load", "°c — very high"],
165        recipe: Recipe {
166            severity: "ACTION",
167            title: "CPU running hot",
168            steps: &[
169                "Shut down and clean dust from fans and heatsink with compressed air — this is the fix 90% of the time",
170                "Check that all fan headers are connected and fans are spinning on boot",
171                "Verify thermal paste on CPU heatsink — if it's more than 4 years old and temperatures are high, repaste",
172                "In BIOS: confirm fan curve is not set to 'Silent' mode — switch to 'Standard' or 'Performance'",
173                "Check for CPU throttling: PowerShell → Get-WmiObject -Class Win32_Processor | Select Name,CurrentClockSpeed,MaxClockSpeed — if Current is much lower than Max under load, it's throttling",
174            ],
175            dig_deeper: Some("thermal"),
176        },
177    },
178
179    // ── Security ──────────────────────────────────────────────────────────────
180    RecipeEntry {
181        triggers: &["real-time protection: disabled", "defender.*disabled", "firewall.*off"],
182        recipe: Recipe {
183            severity: "ACTION",
184            title: "Windows security protection disabled",
185            steps: &[
186                "Re-enable Defender real-time protection: Windows Security → Virus & threat protection → turn on Real-time protection",
187                "If Defender shows as disabled by a third-party antivirus, ensure that AV is up to date and its own real-time protection is on",
188                "Re-enable Windows Firewall: Control Panel → Windows Defender Firewall → Turn Windows Defender Firewall on or off → turn on for all profiles",
189                "Run a quick scan: Windows Security → Virus & threat protection → Quick scan",
190            ],
191            dig_deeper: Some("security"),
192        },
193    },
194    RecipeEntry {
195        triggers: &["threat detected", "quarantine", "malware", "virus found"],
196        recipe: Recipe {
197            severity: "ACTION",
198            title: "Threat detected by Windows Defender",
199            steps: &[
200                "Open Windows Security → Virus & threat protection → Protection history → review detected threats",
201                "If action is 'Quarantined', Defender has contained it — review and remove from quarantine",
202                "Run a full offline scan: Windows Security → Virus & threat protection → Scan options → Microsoft Defender Offline scan",
203                "Change passwords for any accounts accessed on this machine after the infection date",
204                "Check browser extensions for anything you didn't install",
205            ],
206            dig_deeper: Some("defender_quarantine"),
207        },
208    },
209
210    // ── Windows Update ────────────────────────────────────────────────────────
211    RecipeEntry {
212        triggers: &["windows update", "pending update", "update.*required"],
213        recipe: Recipe {
214            severity: "INVESTIGATE",
215            title: "Windows updates pending",
216            steps: &[
217                "Open Settings → Windows Update → Check for updates",
218                "Install all available updates, then restart when prompted",
219                "If updates are stuck: PowerShell (admin) → net stop wuauserv; net stop bits; net start wuauserv; net start bits",
220                "If stuck for more than 24 hours: run the Windows Update Troubleshooter from Settings → System → Troubleshoot → Other troubleshooters",
221            ],
222            dig_deeper: Some("updates"),
223        },
224    },
225
226    // ── Device / driver errors ────────────────────────────────────────────────
227    RecipeEntry {
228        triggers: &["yellow bang", "pnp error", "configmanager error", "error code 43", "error code 10", "error code 28", "device problem", "driver error"],
229        recipe: Recipe {
230            severity: "ACTION",
231            title: "Hardware device error detected",
232            steps: &[
233                "Open Device Manager: press Win+R → type 'devmgmt.msc' → Enter",
234                "Look for yellow exclamation marks (!) — right-click → Properties → note the error code and device name",
235                "Error Code 43 (USB/GPU): unplug and replug the device, or roll back the driver: right-click → Properties → Driver → Roll Back Driver",
236                "Error Code 10 (failed to start): update the driver — right-click → Update driver → Search automatically",
237                "Error Code 28 (no driver): download the driver from the manufacturer's website (look up the device name + Windows version)",
238                "For recurring errors: run SFC scan → PowerShell (admin) → sfc /scannow",
239            ],
240            dig_deeper: Some("device_health"),
241        },
242    },
243
244    // ── No backup configured ──────────────────────────────────────────────────
245    RecipeEntry {
246        triggers: &["file history: disabled", "no backup configured", "no restore points", "last backup: never", "backup: not configured", "file history.*disabled", "no system restore"],
247        recipe: Recipe {
248            severity: "INVESTIGATE",
249            title: "No backup configured",
250            steps: &[
251                "Enable File History: Settings → System → Storage → Advanced storage settings → Backup options → Add a drive",
252                "Enable System Restore: search 'Create a restore point' → select C: → Configure → turn on protection → OK → Create",
253                "For a full image backup: search 'Backup and Restore (Windows 7)' → Create a system image → choose an external drive",
254                "OneDrive Known Folder Backup covers Desktop/Documents/Pictures: Settings → OneDrive → Backup → Manage backup",
255                "Run your first backup immediately — a backup that has never run has zero value",
256            ],
257            dig_deeper: Some("windows_backup"),
258        },
259    },
260
261    // ── SMB1 enabled ─────────────────────────────────────────────────────────
262    RecipeEntry {
263        triggers: &["smb1 is enabled", "smb1: enabled", "smb1 protocol: enabled", "smb version 1", "smbv1 enabled"],
264        recipe: Recipe {
265            severity: "ACTION",
266            title: "SMB1 protocol enabled — security risk",
267            steps: &[
268                "SMB1 is a deprecated protocol exploited by WannaCry and NotPetya ransomware — disable it immediately",
269                "Disable SMB1: PowerShell (admin) → Set-SmbServerConfiguration -EnableSMB1Protocol $false -Force",
270                "Verify it's off: PowerShell → Get-SmbServerConfiguration | Select EnableSMB1Protocol (should show False)",
271                "If a legacy device (old NAS, printer) stops working after disabling, upgrade its firmware or replace it — do not re-enable SMB1",
272                "Restart required to fully remove the SMB1 listener",
273            ],
274            dig_deeper: Some("shares"),
275        },
276    },
277
278    // ── BitLocker not protecting ──────────────────────────────────────────────
279    RecipeEntry {
280        triggers: &["protection state: off", "bitlocker: off", "bitlocker.*not protecting", "encryption status: fully decrypted", "bitlocker.*disabled"],
281        recipe: Recipe {
282            severity: "MONITOR",
283            title: "Drive encryption not enabled",
284            steps: &[
285                "BitLocker encrypts your drive so data is unreadable if the laptop is lost or stolen — strongly recommended on portable machines",
286                "Enable BitLocker: search 'Manage BitLocker' → Turn on BitLocker for C: → follow the wizard",
287                "Save the recovery key to your Microsoft account or print it — you will need it if Windows can't auto-unlock at boot",
288                "Encryption runs in the background and takes 1–3 hours for a typical drive — the PC remains usable during this time",
289                "Requires TPM 1.2+ or USB key; check: PowerShell → Get-Tpm | Select TpmPresent,TpmReady",
290            ],
291            dig_deeper: Some("bitlocker"),
292        },
293    },
294
295    // ── DNS resolution failing ────────────────────────────────────────────────
296    RecipeEntry {
297        triggers: &["dns resolution: failed", "dns: failed", "dns fail", "dns resolution failed", "could not resolve"],
298        recipe: Recipe {
299            severity: "ACTION",
300            title: "DNS resolution failing",
301            steps: &[
302                "Flush DNS cache: PowerShell (admin) → Clear-DnsClientCache",
303                "Test DNS directly: PowerShell → Resolve-DnsName google.com -Server 8.8.8.8 — if this works, your DNS server is the problem",
304                "Switch to a reliable DNS server: PowerShell (admin) → Set-DnsClientServerAddress -InterfaceAlias 'Wi-Fi' -ServerAddresses ('8.8.8.8','1.1.1.1')",
305                "Check if the DNS client service is running: Get-Service Dnscache | Select Status",
306                "If on a corporate network or VPN, contact IT — split DNS may require the VPN to be connected for internal names to resolve",
307            ],
308            dig_deeper: Some("dns_servers"),
309        },
310    },
311
312    // ── Repeated app crashes ──────────────────────────────────────────────────
313    RecipeEntry {
314        triggers: &["faulting application", "crash count", "crash frequency", "application hang", "faulting module"],
315        recipe: Recipe {
316            severity: "INVESTIGATE",
317            title: "Application crashing repeatedly",
318            steps: &[
319                "Note the faulting application name and module from the report — these are the most important clues",
320                "If the faulting module is ntdll.dll or a system DLL: run SFC to repair Windows files → PowerShell (admin) → sfc /scannow",
321                "If the faulting module is a third-party DLL (e.g. a codec or plugin): uninstall the associated program",
322                "Update or reinstall the crashing application — corrupted installs are a common cause",
323                "Check for conflicting software: antivirus, screen recorders, and overlays (Discord, GeForce Experience) frequently inject into other processes",
324                "If it is a Microsoft Office app: run the Office repair → Control Panel → Programs → right-click Office → Change → Quick Repair",
325            ],
326            dig_deeper: Some("app_crashes"),
327        },
328    },
329
330    // ── Visual C++ / runtime missing ─────────────────────────────────────────
331    RecipeEntry {
332        triggers: &["vcruntime", "msvcr", "0xc000007b", "side-by-side configuration", "missing runtime", "vc++ redistributable"],
333        recipe: Recipe {
334            severity: "ACTION",
335            title: "Visual C++ runtime missing or corrupt",
336            steps: &[
337                "Download and install the latest Visual C++ Redistributable packages (both x64 and x86) from Microsoft: search 'Visual C++ Redistributable downloads'",
338                "Install all available years: 2015–2022 package covers most apps; older apps may need 2013, 2012, or 2010 separately",
339                "If a specific app shows error 0xc000007b: right-click the app → Properties → Compatibility → Run as administrator",
340                "Repair existing runtimes: Control Panel → Programs → find 'Microsoft Visual C++ 20XX' → Repair",
341                "After installing, restart before testing the application again — runtimes must be registered at boot",
342            ],
343            dig_deeper: None,
344        },
345    },
346
347    // ── Certificate expiring ──────────────────────────────────────────────────
348    RecipeEntry {
349        triggers: &["expiring within 30 days", "expires in", "certificate expir", "cert.*expir"],
350        recipe: Recipe {
351            severity: "INVESTIGATE",
352            title: "Certificate expiring soon",
353            steps: &[
354                "Open Certificate Manager: press Win+R → type 'certmgr.msc' → check Personal → Certificates for the expiring cert",
355                "Note the certificate subject and issuer — determines who you need to contact for renewal",
356                "For personal/S-MIME certificates: renew through your CA or email provider portal",
357                "For web/TLS certificates on a server: generate a new CSR and submit to your CA before expiry",
358                "For code-signing certificates: do not let these lapse — signed binaries will show 'unknown publisher' warnings after expiry",
359            ],
360            dig_deeper: Some("certificates"),
361        },
362    },
363
364    // ── Wi-Fi weak signal ─────────────────────────────────────────────────────
365    RecipeEntry {
366        triggers: &["signal: poor", "weak signal", "rssi: -8", "rssi: -9", "signal strength: poor", "quality: poor", "poor signal"],
367        recipe: Recipe {
368            severity: "MONITOR",
369            title: "Wi-Fi signal weak",
370            steps: &[
371                "Move closer to the router or access point — Wi-Fi degrades quickly through walls and floors",
372                "Switch to 5 GHz band if available — faster and less congested in most home environments (but shorter range than 2.4 GHz)",
373                "Check for interference: microwave ovens, baby monitors, and neighboring networks on the same channel all degrade signal",
374                "Change the router's Wi-Fi channel: log into router admin → Wireless settings → try channels 1, 6, or 11 (2.4 GHz) or auto (5 GHz)",
375                "Update the Wi-Fi adapter driver: Device Manager → Network Adapters → right-click adapter → Update driver",
376                "If signal is consistently poor from a fixed desk, consider a powerline adapter or mesh Wi-Fi node nearby",
377            ],
378            dig_deeper: Some("wifi"),
379        },
380    },
381
382    // ── NTP / time sync failure ───────────────────────────────────────────────
383    RecipeEntry {
384        triggers: &["time sync failed", "sync failed", "clock drift", "ntp.*error", "w32tm.*fail", "ntp source.*unreachable", "time.*not synchronized"],
385        recipe: Recipe {
386            severity: "INVESTIGATE",
387            title: "System clock not synchronizing",
388            steps: &[
389                "Force a sync now: PowerShell (admin) → w32tm /resync /force",
390                "Check the current NTP source: PowerShell → w32tm /query /source",
391                "If source shows 'Local CMOS Clock' or 'Free-running', the time service has lost its server",
392                "Reset to Microsoft's NTP server: PowerShell (admin) → w32tm /config /manualpeerlist:time.windows.com /syncfromflags:manual /reliable:YES /update",
393                "Restart the time service: PowerShell (admin) → Restart-Service w32tm",
394                "If clock drift is large (>5 minutes), some authentication systems (Kerberos, MFA) will fail until synced",
395            ],
396            dig_deeper: Some("ntp"),
397        },
398    },
399
400    // ── Page file missing ─────────────────────────────────────────────────────
401    RecipeEntry {
402        triggers: &["no page file", "pagefile: none", "page file: none", "virtual memory: none", "pagefile not configured", "no pagefile"],
403        recipe: Recipe {
404            severity: "INVESTIGATE",
405            title: "Page file not configured",
406            steps: &[
407                "Windows needs a page file even with plenty of RAM — some apps and crash dumps require it",
408                "Re-enable automatic page file management: search 'Adjust the appearance and performance of Windows' → Advanced → Virtual memory → Change → check 'Automatically manage'",
409                "If manually set: assign at least 1.5× your RAM as maximum size on the system drive",
410                "After changing page file settings, restart is required — changes do not take effect until reboot",
411                "Note: if this machine intentionally has no page file (e.g. a RAM disk setup), verify that was deliberate before changing it",
412            ],
413            dig_deeper: Some("pagefile"),
414        },
415    },
416
417    // ── System file corruption ────────────────────────────────────────────────
418    RecipeEntry {
419        triggers: &["corrupt files found", "autorepairrequired: true", "integrity.*failed", "component store corruption", "sfc.*corrupt", "windows resource protection found corrupt"],
420        recipe: Recipe {
421            severity: "ACTION",
422            title: "Windows system file corruption detected",
423            steps: &[
424                "Run SFC to repair corrupt files: PowerShell (admin) → sfc /scannow (takes 5–15 minutes)",
425                "If SFC reports 'Windows Resource Protection found corrupt files but was unable to fix some of them', run DISM next:",
426                "DISM repair: PowerShell (admin) → DISM /Online /Cleanup-Image /RestoreHealth (requires internet access, 10–30 minutes)",
427                "Run SFC again after DISM completes — DISM provides the source files SFC needs",
428                "Restart after both complete, then check Event Viewer for CBS log: Applications and Services Logs → Microsoft → Windows → Servicing",
429                "If corruption persists after both tools: in-place upgrade repair (Windows Setup without wiping data) is the next step",
430            ],
431            dig_deeper: Some("integrity"),
432        },
433    },
434
435    // ── Service start failure ─────────────────────────────────────────────────
436    RecipeEntry {
437        triggers: &["stopped unexpectedly", "failed to start", "error 1067", "error 1053", "service terminated", "exited with code", "failed to respond"],
438        recipe: Recipe {
439            severity: "INVESTIGATE",
440            title: "Service failed to start or stopped unexpectedly",
441            steps: &[
442                "Find the failing service name in the report, then check its status: PowerShell → Get-Service <ServiceName>",
443                "Read the specific error from the Application/System event log: Event Viewer → Windows Logs → System → filter for Service Control Manager (Event ID 7034 or 7031)",
444                "Try to start it manually: PowerShell (admin) → Start-Service <ServiceName> — note any error message",
445                "Check if the service account has the right permissions: Services console (services.msc) → right-click → Properties → Log On tab",
446                "Look for a dependent service that failed first — a service won't start if something it requires is stopped",
447                "If the service EXE is missing or corrupt, reinstall the application that owns it",
448            ],
449            dig_deeper: Some("services"),
450        },
451    },
452
453    // ── RDP unreachable ───────────────────────────────────────────────────────
454    RecipeEntry {
455        triggers: &["fdenytsconnections: 1", "no enabled rdp firewall", "rdp status: disabled"],
456        recipe: Recipe {
457            severity: "ACTION",
458            title: "Remote Desktop (RDP) is disabled or blocked",
459            steps: &[
460                "Enable RDP: Settings → System → Remote Desktop → Enable Remote Desktop (or PowerShell admin: Set-ItemProperty 'HKLM:\\System\\CurrentControlSet\\Control\\Terminal Server' fDenyTSConnections 0)",
461                "Ensure the RDP firewall rule is enabled: PowerShell (admin) → Enable-NetFirewallRule -DisplayGroup 'Remote Desktop'",
462                "Verify port 3389 is listening after enabling: PowerShell → netstat -an | findstr 3389",
463                "If NLA is required, make sure the connecting user account has the right to log in remotely (must be in Remote Desktop Users group or Administrators)",
464                "Check that Windows Firewall is not blocking the connection — on the host, temporarily allow pings to confirm network path is open",
465                "For cloud VMs: check the security group / NSG allows inbound TCP 3389 from your IP",
466            ],
467            dig_deeper: Some("rdp"),
468        },
469    },
470
471    // ── Windows Update service broken ─────────────────────────────────────────
472    RecipeEntry {
473        triggers: &["wuauserv: stopped", "wuauserv stopped", "windows update: stopped", "update service stopped", "bits: stopped", "bits stopped"],
474        recipe: Recipe {
475            severity: "ACTION",
476            title: "Windows Update service is stopped or broken",
477            steps: &[
478                "Run the Windows Update Troubleshooter: Settings → Update & Security → Troubleshoot → Additional troubleshooters → Windows Update",
479                "Manually restart the update services: PowerShell (admin) → Stop-Service wuauserv, bits, cryptsvc, msiserver → Start-Service wuauserv, bits, cryptsvc",
480                "Clear the update cache if stuck: PowerShell (admin) → Stop-Service wuauserv → Remove-Item C:\\Windows\\SoftwareDistribution\\* -Recurse -Force → Start-Service wuauserv",
481                "Check for conflicting 3rd-party update tools (WSUS, SCCM, Intune policies) that may be disabling updates",
482                "Run the System Update Readiness Tool: DISM /Online /Cleanup-Image /RestoreHealth",
483                "If the service keeps stopping, check Event Viewer → Windows Logs → System for Windows Update Agent errors around the same time",
484            ],
485            dig_deeper: Some("updates"),
486        },
487    },
488
489    // ── PrintNightmare not mitigated ──────────────────────────────────────────
490    RecipeEntry {
491        triggers: &["rpcauthnlevelprivacyenabled: 0", "printnightmare rpc mitigation not applied", "point and print allows silent", "finding: printnightmare"],
492        recipe: Recipe {
493            severity: "INVESTIGATE",
494            title: "PrintNightmare (CVE-2021-34527) mitigation not applied",
495            steps: &[
496                "Apply the RPC authentication hardening fix: PowerShell (admin) → Set-ItemProperty 'HKLM:\\SYSTEM\\CurrentControlSet\\Control\\Print' -Name RpcAuthnLevelPrivacyEnabled -Value 1 -Type DWord",
497                "Restrict Point and Print driver installs to administrators: PowerShell (admin) → Set-ItemProperty 'HKLM:\\SOFTWARE\\Policies\\Microsoft\\Windows NT\\Printers\\PointAndPrint' -Name RestrictDriverInstallationToAdministrators -Value 1 -Type DWord",
498                "If the Print Spooler is not needed (e.g. server, workstation that never prints remotely): PowerShell (admin) → Stop-Service Spooler → Set-Service Spooler -StartupType Disabled",
499                "Verify patch KB5004945 or later is installed: check Windows Update history for July 2021 security updates",
500                "Restart the Spooler service after registry changes: PowerShell (admin) → Restart-Service Spooler",
501            ],
502            dig_deeper: Some("print_spooler"),
503        },
504    },
505];
506
507pub struct HealthScore {
508    pub grade: char,
509    pub label: &'static str,
510    pub action_count: usize,
511    pub investigate_count: usize,
512    pub monitor_count: usize,
513}
514
515impl HealthScore {
516    pub fn summary_line(&self) -> String {
517        match (
518            self.action_count,
519            self.investigate_count,
520            self.monitor_count,
521        ) {
522            (0, 0, 0) => "No issues found — machine is healthy.".to_string(),
523            (0, 0, m) => format!("{} item(s) to monitor.", m),
524            (0, i, 0) => format!("{} item(s) need investigation.", i),
525            (0, i, m) => format!("{} item(s) need investigation, {} to monitor.", i, m),
526            (a, 0, 0) => format!("{} item(s) require immediate action.", a),
527            (a, i, _) => format!(
528                "{} item(s) require immediate action, {} need investigation.",
529                a, i
530            ),
531        }
532    }
533
534    /// A plain-English sentence for non-technical users that explains what
535    /// the grade means and what to do next.
536    pub fn grade_intro(&self) -> &'static str {
537        match self.grade {
538            'A' => "Your PC is in great shape — no issues were found. The diagnostic data below is included for reference.",
539            'B' => "Your PC is doing well, but there's one thing worth a closer look. The action plan below has specific steps.",
540            'C' => "Your PC needs some attention. A couple of things should be investigated — follow the action plan below.",
541            'D' => "Your PC needs attention. There are issues that should be fixed — follow the action plan below.",
542            _ => "Your PC has critical issues that need immediate attention. Work through the action plan below as soon as possible.",
543        }
544    }
545}
546
547/// Compute a health grade (A–F) from diagnostic output sections.
548pub fn score_health(outputs: &[(&str, &str)]) -> HealthScore {
549    let mut all_recipes: Vec<&Recipe> = Vec::new();
550    let mut seen_titles = std::collections::HashSet::new();
551
552    for (_label, output) in outputs {
553        for recipe in match_recipes(output) {
554            if seen_titles.insert(recipe.title) {
555                all_recipes.push(recipe);
556            }
557        }
558    }
559
560    let action_count = all_recipes
561        .iter()
562        .filter(|r| r.severity == "ACTION")
563        .count();
564    let investigate_count = all_recipes
565        .iter()
566        .filter(|r| r.severity == "INVESTIGATE")
567        .count();
568    let monitor_count = all_recipes
569        .iter()
570        .filter(|r| r.severity == "MONITOR")
571        .count();
572
573    let (grade, label) = if action_count >= 3 {
574        ('F', "Critical")
575    } else if action_count >= 1 {
576        ('D', "Poor")
577    } else if investigate_count >= 2 {
578        ('C', "Fair")
579    } else if investigate_count >= 1 {
580        ('B', "Good")
581    } else {
582        ('A', "Excellent")
583    };
584
585    HealthScore {
586        grade,
587        label,
588        action_count,
589        investigate_count,
590        monitor_count,
591    }
592}
593
594/// Format all matching recipes for a given diagnostic output into a
595/// human-readable action plan section suitable for a Markdown report.
596pub fn format_action_plan(outputs: &[(&str, &str)]) -> String {
597    let mut all_recipes: Vec<&Recipe> = Vec::new();
598    let mut seen_titles = std::collections::HashSet::new();
599
600    for (_label, output) in outputs {
601        for recipe in match_recipes(output) {
602            if seen_titles.insert(recipe.title) {
603                all_recipes.push(recipe);
604            }
605        }
606    }
607
608    if all_recipes.is_empty() {
609        return "No actionable findings — machine appears healthy.\n".to_string();
610    }
611
612    // Sort: ACTION first, then INVESTIGATE, then MONITOR
613    all_recipes.sort_by_key(|r| match r.severity {
614        "ACTION" => 0,
615        "INVESTIGATE" => 1,
616        _ => 2,
617    });
618
619    let mut out = String::new();
620    for (i, recipe) in all_recipes.iter().enumerate() {
621        let badge = match recipe.severity {
622            "ACTION" => "⚠ ACTION REQUIRED",
623            "INVESTIGATE" => "🔍 INVESTIGATE",
624            _ => "📊 MONITOR",
625        };
626        out.push_str(&format!("### {}. {} — {}\n\n", i + 1, badge, recipe.title));
627        for step in recipe.steps {
628            out.push_str(&format!("- {}\n", step));
629        }
630        if let Some(_topic) = recipe.dig_deeper {
631            out.push_str("\n*Run `hematite --diagnose` for a deeper automated investigation.*\n");
632        }
633        out.push('\n');
634    }
635
636    out
637}
638
639/// Format all matching recipes as an HTML fragment for embedding in a report page.
640pub fn format_action_plan_html(outputs: &[(&str, &str)]) -> String {
641    let mut all_recipes: Vec<&Recipe> = Vec::new();
642    let mut seen_titles = std::collections::HashSet::new();
643
644    for (_label, output) in outputs {
645        for recipe in match_recipes(output) {
646            if seen_titles.insert(recipe.title) {
647                all_recipes.push(recipe);
648            }
649        }
650    }
651
652    if all_recipes.is_empty() {
653        return "<p class=\"healthy\">No actionable findings — machine appears healthy.</p>\n"
654            .to_string();
655    }
656
657    all_recipes.sort_by_key(|r| match r.severity {
658        "ACTION" => 0,
659        "INVESTIGATE" => 1,
660        _ => 2,
661    });
662
663    let mut out = String::new();
664    for (i, recipe) in all_recipes.iter().enumerate() {
665        let (sev_class, badge_class, badge_text) = match recipe.severity {
666            "ACTION" => ("sev-action", "b-action", "ACTION REQUIRED"),
667            "INVESTIGATE" => ("sev-investigate", "b-investigate", "INVESTIGATE"),
668            _ => ("sev-monitor", "b-monitor", "MONITOR"),
669        };
670        out.push_str(&format!("<div class=\"recipe {}\">\n", sev_class));
671        out.push_str(&format!(
672            "<h3><span class=\"badge {}\">{}</span> {}. {}</h3>\n",
673            badge_class,
674            badge_text,
675            i + 1,
676            he(recipe.title)
677        ));
678        out.push_str("<ol>\n");
679        for step in recipe.steps {
680            out.push_str(&format!("<li>{}</li>\n", he(step)));
681        }
682        out.push_str("</ol>\n");
683        if let Some(_topic) = recipe.dig_deeper {
684            out.push_str(
685                "<p class=\"dig-deeper\">Run <code>hematite --diagnose</code> for a deeper automated investigation of this issue.</p>\n"
686            );
687        }
688        out.push_str("</div>\n");
689    }
690    out
691}
692
693use crate::agent::html_template::he;