# 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 |
