Skip to main content

hematite/agent/
fix_recipes.rs

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