docs: C-1 Windows-specific bugs audit

Comprehensive audit of Windows agent code: winget detection,
Windows Update ghost updates, service wrapper, HWID, and
vendored windowsupdate package.

Key findings:
- F-C1-1 HIGH: Winget not found as SYSTEM (PATH-only search)
- F-C1-3 HIGH: No post-install verification (ghost updates)
- F-C1-5 HIGH: Windows service has duplicated polling loop
  missing B-2 fixes (jitter cap, exponential backoff)
- F-C1-2 MEDIUM: Fragile winget text parser
- F-C1-4 MEDIUM: No service auto-restart on crash

9 findings total. See docs/C1_Windows_Audit.md for details.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-29 08:22:18 -04:00
parent f71f878a35
commit 799c155d94

205
docs/C1_Windows_Audit.md Normal file
View File

@@ -0,0 +1,205 @@
# C-1 Windows-Specific Bugs Audit
**Date:** 2026-03-29
**Branch:** culurien
**Scope:** Winget detection, ghost updates, service wrapper, HWID, vendored package
---
## 1. WINDOWS-SPECIFIC FILES
| File | Build Tag | Purpose |
|------|-----------|---------|
| `scanner/winget.go` | none (cross-platform) | Winget package update scanning |
| `scanner/windows_wua.go` | `//go:build windows` | WUA COM API scanning |
| `scanner/windows_override.go` | `//go:build windows` | Type alias for WUA scanner |
| `scanner/windows.go` | `//go:build !windows` | Stub (non-Windows) |
| `service/windows.go` | `//go:build windows` | Windows service wrapper |
| `system/windows.go` | `//go:build windows` | System info collection |
| `system/windows_stub.go` | (no tag — always compiles) | Stub for non-Windows |
| `installer/windows.go` | none (cross-platform) | Windows Update installer |
| `installer/winget.go` | none (cross-platform) | Winget package installer |
| `pkg/windowsupdate/*` | none (20 files) | Vendored WUA COM bindings |
---
## 2. WINGET DETECTION & SCANNING
### 2a. Winget Location
`scanner/winget.go:41`: `exec.LookPath("winget")` — searches PATH only.
**F-C1-1 HIGH: Winget not found when running as SYSTEM service.**
`exec.LookPath` searches the PATH environment variable. When the agent runs as SYSTEM via the Windows service, the PATH does not include the per-user `%LOCALAPPDATA%\Microsoft\WindowsApps\` directory where winget is typically installed. The scanner will always report "winget is not available" when running as a service.
Known winget install locations NOT checked:
- `%LOCALAPPDATA%\Microsoft\WindowsApps\winget.exe` (per-user)
- `%PROGRAMFILES%\WindowsApps\Microsoft.DesktopAppInstaller_*\winget.exe` (system-wide)
### 2b. Command Used
`scanner/winget.go:90`: `winget list --outdated --accept-source-agreements --output json`
Fallback: `winget list --outdated --accept-source-agreements` (text output, line 126)
### 2c. Output Parsing
- **Primary**: JSON parsing via `json.Unmarshal` into `[]WingetPackage` (line 104)
- **Fallback**: Text parsing via `strings.Fields` (line 149)
**F-C1-2 MEDIUM: Fragile text parser.** The fallback text parser at line 149 uses `strings.Fields(line)` and assumes `fields[0]` = name, `fields[1]` = version, `fields[2]` = available. Winget table output has variable-width columns with spaces IN package names (e.g., "Microsoft Visual Studio Code"). This parser will split "Microsoft Visual Studio Code 1.85.0 1.86.0" into 6 fields, misidentifying the name as just "Microsoft".
### 2d. Data Structure
`WingetPackage` struct (line 15-23): Name, ID, Version, Available, Source, IsPinned, PinReason.
### 2e. Edge Cases
- **No updates**: JSON returns `[]` → empty result, correct.
- **Format change**: JSON output change would cause `json.Unmarshal` error → falls back to text parser → likely misparses.
- **UAC prompt**: `--accept-source-agreements` flag suppresses most prompts. But `--output json` flag was added in winget v1.6+ — older versions will fail.
- **SYSTEM account**: See F-C1-1.
### 2f. Tests
No winget parsing tests exist in the codebase.
---
## 3. WINDOWS UPDATE SCANNING (GHOST UPDATES)
### 3a-3c. COM Interfaces
Uses vendored `pkg/windowsupdate/` package (originally by Zheng Dayu, Apache 2.0 license).
- `IUpdateSession``CreateUpdateSearcher()`
- `IUpdateSearcher``Search("IsInstalled=0 AND IsHidden=0")`
- `ISearchResult``Updates` collection
- Each `IUpdate` has: Title, Description, Identity, MsrcSeverity, Categories, KBArticleIDs, SecurityBulletinIDs, IsInstalled, IsHidden, IsMandatory, etc.
### 3d. Installation Flow
`installer/windows.go` uses PowerShell (`Install-WindowsUpdate`) or `wuauclt /detectnow` + `wuauclt /installnow`.
**F-C1-3 HIGH: No post-install re-scan with state verification.**
After installation, the agent does NOT re-scan to verify `IsInstalled=1`. The next scan cycle uses `IsInstalled=0 AND IsHidden=0` which may still return the update if Windows hasn't committed the install state yet (common after reboot-pending updates).
### 3e-3f. Timing Issue
The ghost update bug is a timing issue:
1. Agent installs update via PowerShell/wuauclt
2. Agent immediately re-scans on next polling cycle (5 seconds in rapid mode)
3. Windows Update has not yet committed the install state
4. `IsInstalled=0` still returns true for the just-installed update
5. Agent reports it as "available" again
**Root cause**: No delay or state verification between install and next scan. No `IsInstalled` check post-install.
### 3g. IsInstalled / IsHidden
The search criteria `IsInstalled=0 AND IsHidden=0` is correct for finding available updates. But after installation, the `IsInstalled` flag transitions asynchronously — especially for updates requiring a reboot. During the reboot-pending window, `IsInstalled` may still be `0`.
### 3h. Vendored Package Modifications
The vendored package appears to be the original Zheng Dayu library with additions:
- `QueryHistoryAll()` was added (not in the original)
- Additional fields on `IUpdate` (SecurityBulletinIDs, MsrcSeverity, etc.)
- No modifications to core COM interaction logic
---
## 4. WINDOWS SERVICE WRAPPER
### 4a. Framework
`golang.org/x/sys/windows/svc` — official Go Windows service package. Uses `svc.Run()` for service lifecycle.
### 4b. Service Account
Runs as SYSTEM (default for `sc.exe` created services). The install function at `service/windows.go` uses `mgr.Config{StartType: mgr.StartAutomatic}` without specifying a user account.
### 4c. Permission Issues
- **Windows Update COM**: SYSTEM CAN access WUA APIs — this works.
- **Winget**: SYSTEM CANNOT access per-user winget installation — see F-C1-1.
- **C:\ProgramData\RedFlag\**: SYSTEM has full access — this works.
### 4d. Service Installation
`service/windows.go` contains `InstallService()` and `RemoveService()` functions using `mgr.Connect()``mgr.CreateService()`. Agent binary provides `--install-service` and `--remove-service` CLI flags.
### 4e. Crash Recovery
**F-C1-4 MEDIUM: No auto-restart on service crash.** The service is created with `mgr.Config{StartType: mgr.StartAutomatic}` but no recovery options (FailureActions). If the service crashes, it stays stopped until manually restarted or system rebooted.
### F-C1-5 HIGH: Windows service has duplicated polling loop.
`service/windows.go:138-178` contains a COMPLETE COPY of the agent polling loop (`runAgent()`). This is a separate implementation from `cmd/agent/main.go`. The B-2 fixes (proportional jitter F-B2-5, exponential backoff F-B2-7) were applied to `cmd/agent/main.go` but NOT to `service/windows.go:runAgent()`. The Windows service still has the old fixed 30-second jitter (line 178) and no exponential backoff.
---
## 5. WINDOWS MACHINE ID (HWID)
### 5a. HWID Source
`system/machine_id.go:80-88`: Uses `machineid.ID()` from `denisbrodbeck/machineid` library. On Windows, this reads `HKLM\SOFTWARE\Microsoft\Cryptography\MachineGuid` from the registry.
### 5b. Hashing
YES — `hashMachineID(id)` returns SHA256 hex (line 37-39). Same as Linux.
### 5c. HWID Change
If MachineGuid changes (VM clone, sysprep, registry corruption), the agent gets a different machine ID → MachineBindingMiddleware rejects it → agent must re-register.
### 5d. WMI Unavailability
Not applicable — the library uses registry, not WMI. If the registry key is missing, `machineid.ID()` fails → falls back to `generateGenericMachineID()` (hostname-based).
### 5e. Cross-Platform Consistency
Uses the same `GetMachineID()` function. Windows fallback is simpler than Linux (just retries `machineid.ID()`, no additional registry keys tried). Same issue flagged in DEV-024.
---
## 6. CROSS-PLATFORM CONSISTENCY
### 6a. Update Schema
Both Windows and Linux produce `client.UpdateReportItem` with the same struct. Package type differentiators: `"winget"`, `"windows_update"`, `"apt"`, `"dnf"`.
### 6b. Machine ID Format
Both produce SHA256 hex strings (64 chars). Consistent.
### 6c. OS Detection
`Agent.OSType` is set during registration from `runtime.GOOS`. Server stores it in `agents.os_type` column with CHECK constraint: `('windows', 'linux', 'macos')`.
### 6d. Config Paths
`constants/paths.go`: Windows uses `C:\ProgramData\RedFlag\`, Linux uses `/etc/redflag/`. Handled via `runtime.GOOS` switch. Correct.
---
## 7. ETHOS VIOLATIONS
- **ETHOS #1**: Winget scanner uses `fmt.Printf` for error output (lines 59, 67, 72-77, etc.) instead of structured logging. Not using `[TAG] [system] [component]` format.
- **ETHOS #1**: Windows service `runAgent()` uses emojis in log messages (lines 139-144).
- **ETHOS #3**: `installViaWuauclt` (installer/windows.go:127) runs `wuauclt /detectnow` followed by `wuauclt /installnow` with a fixed 10-second sleep between them, assuming detection completes in 10 seconds.
---
## FINDINGS SUMMARY
| ID | Severity | Finding | Location |
|----|----------|---------|----------|
| F-C1-1 | HIGH | Winget not found when running as SYSTEM (searches PATH only, not known install locations) | scanner/winget.go:41 |
| F-C1-2 | MEDIUM | Winget text fallback parser splits on whitespace, breaks on package names with spaces | scanner/winget.go:149 |
| F-C1-3 | HIGH | No post-install state verification for Windows Updates — causes ghost updates | scanner/windows_wua.go:58, installer/windows.go |
| F-C1-4 | MEDIUM | Windows service has no auto-restart on crash (no FailureActions set) | service/windows.go (InstallService) |
| F-C1-5 | HIGH | Windows service runAgent() is a duplicated polling loop missing B-2 fixes (jitter, backoff) | service/windows.go:138-178 |
| F-C1-6 | LOW | Winget scanner uses fmt.Printf instead of structured logging (ETHOS #1) | scanner/winget.go:59,67,72 |
| F-C1-7 | LOW | Windows service runAgent() uses emojis in log messages (ETHOS #1) | service/windows.go:139-144 |
| F-C1-8 | LOW | No winget parsing tests in codebase | scanner/winget.go |
| F-C1-9 | LOW | Windows HWID fallback is simpler than Linux (only retries machineid.ID, no registry key exploration) | system/machine_id.go:80-88 |