Skip to content

Commit 51761f0

Browse files
feat: Harden against tampering; add protected install and quiet SYSTEM updates (v1.5.0)
Tamper resistance: - Relocate monitor.db and events.log to %ProgramData%\SystemHelper and have the GameHost watchdog lock them (deny-delete + OWNER RIGHTS cap) so a standard user cannot delete usage history or logs even while DeviceMon is closed. - Keep SYSTEM-only backups and auto-restore the database / DeviceMon.exe if missing. - Reduce watchdog check interval from 15s to 5s. Config resilience: - A corrupt appsettings.json no longer crashes startup or the dashboard; fall back to a last-known-good copy and then built-in defaults. The web host no longer aborts on an unparseable config file. Protected install and updates: - Add install.ps1 to install into C:\Program Files\DeviceMon (read-only to the child) and optionally configure a trusted update source. - Add quiet, admin-level updates performed by the SYSTEM watchdog from a fixed trusted source, triggered from the dashboard (no UAC, remote-capable, and the child cannot redirect the update source). Housekeeping: - Bump version to 1.5.0 across all projects; add RELEASE_NOTES_v1.05.md; update README. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent f3e83ab commit 51761f0

14 files changed

Lines changed: 768 additions & 41 deletions

Data/UsageDatabase.cs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using Microsoft.Data.Sqlite;
22
using MonitorAndControl.Models;
3+
using MonitorAndControl.Services;
34

45
namespace MonitorAndControl.Data;
56

@@ -14,11 +15,9 @@ public UsageDatabase(string? dbPath = null)
1415
{
1516
if (string.IsNullOrWhiteSpace(dbPath))
1617
{
17-
var folder = Path.Combine(
18-
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
19-
"SystemHelper");
20-
Directory.CreateDirectory(folder);
21-
dbPath = Path.Combine(folder, "monitor.db");
18+
// SYSTEM-protected data directory (see AppPaths) - the watchdog denies
19+
// Delete on monitor.db so a standard user cannot wipe usage history.
20+
dbPath = AppPaths.DatabasePath;
2221
}
2322
else
2423
{

MonitorAndControl.csproj

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@
1414
<Product>DeviceMon</Product>
1515
<Description>Local parental monitoring and app limit dashboard</Description>
1616
<AssemblyTitle>DeviceMon</AssemblyTitle>
17-
<Version>1.4.0</Version>
18-
<AssemblyVersion>1.4.0.0</AssemblyVersion>
19-
<FileVersion>1.4.0.0</FileVersion>
17+
<Version>1.5.0</Version>
18+
<AssemblyVersion>1.5.0.0</AssemblyVersion>
19+
<FileVersion>1.5.0.0</FileVersion>
2020
</PropertyGroup>
2121

2222
<ItemGroup>

PopupHost/PopupHost.csproj

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77
<Nullable>enable</Nullable>
88
<ImplicitUsings>enable</ImplicitUsings>
99
<UseWindowsForms>true</UseWindowsForms>
10-
<Version>1.4.0</Version>
11-
<AssemblyVersion>1.4.0.0</AssemblyVersion>
12-
<FileVersion>1.4.0.0</FileVersion>
10+
<Version>1.5.0</Version>
11+
<AssemblyVersion>1.5.0.0</AssemblyVersion>
12+
<FileVersion>1.5.0.0</FileVersion>
1313
</PropertyGroup>
1414

1515
</Project>

Program.cs

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,17 +96,72 @@ static void Main(string[] args)
9696
private static AppConfig LoadConfig()
9797
{
9898
var path = GetConfigPath();
99+
var lkgPath = Path.Combine(AppPaths.DataDir, "appsettings.lkg.json");
99100

101+
// Primary source: the on-disk appsettings.json. A corrupt or unreadable
102+
// file must never crash startup - a crash here just makes the watchdog
103+
// relaunch DeviceMon into the same crash, disabling monitoring entirely.
100104
if (File.Exists(path))
101105
{
102-
var json = File.ReadAllText(path);
103-
_cachedConfig = JsonSerializer.Deserialize<AppConfig>(json) ?? new AppConfig();
104-
return _cachedConfig;
106+
try
107+
{
108+
var json = File.ReadAllText(path);
109+
var config = JsonSerializer.Deserialize<AppConfig>(json)
110+
?? throw new InvalidOperationException("Configuration deserialized to null.");
111+
_cachedConfig = config;
112+
SaveLastKnownGoodConfig(lkgPath, json);
113+
return _cachedConfig;
114+
}
115+
catch (Exception ex)
116+
{
117+
Logger.Instance.Error($"appsettings.json is invalid ({ex.Message}); falling back to last-known-good or defaults.");
118+
}
119+
}
120+
else
121+
{
122+
Logger.Instance.Warn("appsettings.json not found; using last-known-good configuration or defaults.");
105123
}
124+
125+
// Fallback 1: the last-known-good copy saved after the most recent valid load.
126+
if (File.Exists(lkgPath))
127+
{
128+
try
129+
{
130+
var config = JsonSerializer.Deserialize<AppConfig>(File.ReadAllText(lkgPath));
131+
if (config != null)
132+
{
133+
_cachedConfig = config;
134+
Logger.Instance.Info("Loaded configuration from last-known-good backup.");
135+
return _cachedConfig;
136+
}
137+
}
138+
catch (Exception ex)
139+
{
140+
Logger.Instance.Error($"Last-known-good configuration is unusable ({ex.Message}); using defaults.");
141+
}
142+
}
143+
144+
// Fallback 2: built-in defaults. Most runtime settings live in the database
145+
// (which is authoritative after first run), so DeviceMon keeps working.
106146
_cachedConfig = new AppConfig();
107147
return _cachedConfig;
108148
}
109149

150+
private static void SaveLastKnownGoodConfig(string lkgPath, string json)
151+
{
152+
try
153+
{
154+
var dir = Path.GetDirectoryName(lkgPath);
155+
if (!string.IsNullOrEmpty(dir))
156+
Directory.CreateDirectory(dir);
157+
File.WriteAllText(lkgPath, json);
158+
}
159+
catch (Exception ex)
160+
{
161+
Logger.Instance.Error($"Failed to save last-known-good configuration: {ex.Message}");
162+
}
163+
}
164+
110165
private static string GetConfigPath()
111166
{
112167
var path = Path.Combine(AppContext.BaseDirectory, "appsettings.json");

README.md

Lines changed: 52 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -78,18 +78,28 @@ Monitor & Control consists of three executables:
7878

7979
When you run `DeviceMon.exe`, it automatically looks for `GameHost.exe` in the same folder. If the watchdog service is not installed (or needs updating), DeviceMon silently asks for administrator privileges via a UAC prompt and installs/updates it. After that:
8080
- The watchdog runs as a Windows service named `GameHost`
81-
- It checks every 15 seconds whether `DeviceMon.exe` is still running
81+
- It checks every 5 seconds whether `DeviceMon.exe` is still running
8282
- If the monitor process is missing, the watchdog relaunches it in the active user session
8383
- No further UAC prompts — the service runs under the SYSTEM account
8484
- If you ever want to remove it, see the [Watchdog uninstall](#installing-the-watchdog-optional-requires-admin) section
8585

8686
### Data storage
8787

88-
- **SQLite database** at `%LOCALAPPDATA%\SystemHelper\monitor.db` — usage records, limits, schedules, app tracking policies, settings
89-
- **Log file** at `%LOCALAPPDATA%\SystemHelper\monitor.log`
88+
- **SQLite database** at `C:\ProgramData\SystemHelper\monitor.db` — usage records, limits, schedules, app tracking policies, settings
89+
- **Log file** at `C:\ProgramData\SystemHelper\events.log`
9090
- **Watchdog log** at `C:\ProgramData\SystemHelper\watchdog.log`
9191
- **Configuration** at `appsettings.json` (alongside the exe)
9292

93+
> **Tamper protection:** The GameHost watchdog (running as SYSTEM) locks `monitor.db`
94+
> and `events.log` so a standard (child) user can read/write them but **cannot delete
95+
> or rename them** — closing DeviceMon no longer lets them wipe usage history. The
96+
> watchdog also keeps SYSTEM-only backups under `C:\ProgramData\SystemHelper\Protected\`
97+
> and restores the database (and `DeviceMon.exe`) if either goes missing. This
98+
> protection is released automatically when GameHost is uninstalled.
99+
>
100+
> *(Older installs stored these files under `%LOCALAPPDATA%\SystemHelper\`; they are
101+
> migrated to the protected location automatically on first launch of this version.)*
102+
93103
### Usage tracking modes
94104

95105
Tracking behavior is configured independently for every process using the **Background** and **Filter overlays** columns in the Limits table. Both options are off by default. The checkboxes save immediately and do not require the application to have a daily limit.
@@ -140,7 +150,7 @@ Usage recorded by an older release has only a total and cannot be reconstructed
140150

141151
Existing databases are upgraded automatically. Tracking policies are also included in configuration exports and restored during import.
142152

143-
`DefaultLimits` and `Schedule` from `appsettings.json` are imported only when the SQLite database is created for the first time. After initialization, the database is authoritative: limits or schedules removed in the dashboard remain removed after restart. Delete `%LOCALAPPDATA%\SystemHelper\monitor.db` only when you intentionally want a fresh first-start import.
153+
`DefaultLimits` and `Schedule` from `appsettings.json` are imported only when the SQLite database is created for the first time. After initialization, the database is authoritative: limits or schedules removed in the dashboard remain removed after restart. To force a fresh first-start import, delete `C:\ProgramData\SystemHelper\monitor.db` — but note the watchdog protects this file (see [Data storage](#data-storage)), so you must first uninstall GameHost (as admin) to release the lock.
144154

145155
---
146156

@@ -284,11 +294,43 @@ Place this file alongside `DeviceMon.exe`. All settings are optional — default
284294

285295
> **Security note:** When remote access is enabled, remote users cannot open the dashboard until an admin password has been created from a trusted local dashboard. After setup, remote users are redirected to the login page. Successful authentication creates an HTTP-only session cookie; use **Settings → Logout** to end that browser session. The built-in dashboard listener is HTTP only; do not expose it beyond a trusted LAN unless you put it behind a TLS reverse proxy, VPN/tunnel, or a trusted local certificate setup.
286296
287-
### Installing the Watchdog (optional, requires admin)
297+
### Recommended install: protected Program Files location (requires admin)
298+
299+
For a child's PC, install DeviceMon into `C:\Program Files\DeviceMon` so the child **cannot delete or modify the application files**. Program Files grants standard users read-and-execute only, so the child can still be forced to close DeviceMon (Windows always allows ending your own process), but they cannot delete `DeviceMon.exe`, `GameHost.exe`, the DLLs, or `appsettings.json` — and the watchdog relaunches DeviceMon within ~5 seconds.
300+
301+
```powershell
302+
# Run PowerShell as Administrator from the publish folder, then:
303+
.\install.ps1
304+
305+
# Or specify source/target explicitly:
306+
.\install.ps1 -SourceDir "C:\path\to\publish" -InstallDir "C:\Program Files\DeviceMon"
307+
```
308+
309+
This copies the app to the protected folder and installs the `GameHost` watchdog pointing at it. The usage database and logs stay in `C:\ProgramData\SystemHelper` (writable so usage can be recorded, but protected against deletion — see [Data storage](#data-storage)).
310+
311+
#### Updating a Program Files install
312+
313+
Because the app folder is read-only to standard users, the in-dashboard updater can't write to it directly. You have two options:
314+
315+
1. **Re-run `install.ps1`** as Administrator against the new publish folder — it stops the watchdog, replaces the files, and restarts everything.
316+
317+
2. **Quiet updates from the dashboard** (no admin prompt, works remotely) — configure a trusted update source at install time:
318+
319+
```powershell
320+
# HTTPS zip (SHA-256 required):
321+
.\install.ps1 -UpdateSource "https://example.com/MonitorAndControl-v1.05.zip" -UpdateSha256 "<hash>"
322+
323+
# …or a folder / network share:
324+
.\install.ps1 -UpdateSource "\\server\share\DeviceMon" -UpdateUsername "user" -UpdatePassword "pw"
325+
```
326+
327+
The source is stored in a SYSTEM-only file (`C:\ProgramData\SystemHelper\Protected\update-source.json`) that the child cannot read or change. When you click **Settings → Update**, the dashboard just *triggers* an update; the `GameHost` service (running as SYSTEM) performs it silently from that fixed source and relaunches DeviceMon in the child's session. Because the source is fixed, the child cannot redirect updates to malicious files — the dashboard never gets to choose where SYSTEM pulls from.
328+
329+
### Installing only the Watchdog (optional, requires admin)
288330

289-
**Normally you don't need to do this manually** — when you run `DeviceMon.exe`, it auto-detects `GameHost.exe` in the same folder and offers to install the watchdog service with a single UAC prompt.
331+
**If you are not using `install.ps1`,** you normally don't need to do this manually either — when you run `DeviceMon.exe`, it auto-detects `GameHost.exe` in the same folder and offers to install the watchdog service with a single UAC prompt.
290332

291-
Manual install is only needed if you want to install the watchdog separately (e.g., deploying to a different folder after the fact):
333+
Manual watchdog install is only needed if you want to install it separately (e.g., deploying to a different folder after the fact):
292334

293335
```powershell
294336
# Run PowerShell as Administrator, then:
@@ -405,10 +447,10 @@ A: Run PowerShell **as Administrator**. The service requires elevation to create
405447
A: Set `EnableRemoteDashboard` to `true` in the `appsettings.json` beside `DeviceMon.exe`, then restart DeviceMon. The app binds to all LAN interfaces when remote mode is enabled. Ensure the Windows Firewall allows port 5000.
406448

407449
**Q: How do I reset all usage data?**
408-
A: Delete the SQLite database at `%LOCALAPPDATA%\SystemHelper\monitor.db` while the monitor is not running. It will be recreated on next launch.
450+
A: The database is tamper-protected while GameHost is installed (a standard user, and even an admin, cannot delete it). To wipe it, uninstall the GameHost watchdog as administrator (this releases the lock), then delete `C:\ProgramData\SystemHelper\monitor.db` while DeviceMon is not running. It is recreated on next launch.
409451

410452
**Q: Where are logs stored?**
411-
A: `%LOCALAPPDATA%\SystemHelper\monitor.log` for the main app, and `C:\ProgramData\SystemHelper\watchdog.log` for the watchdog service.
453+
A: `C:\ProgramData\SystemHelper\events.log` for the main app, and `C:\ProgramData\SystemHelper\watchdog.log` for the watchdog service.
412454

413455
**Q: Can I run it without the web dashboard?**
414456
A: No. The web dashboard is the primary UI.
@@ -462,7 +504,7 @@ A: The **watchdog service** (`GameHost.exe`) runs under SYSTEM account and track
462504
A: Yes. Click the **Pause** button on the dashboard **Live** tab or from the system tray icon context menu. Click **Resume** to continue tracking.
463505

464506
**Q: What happens when I close DeviceMon.exe?**
465-
A: If the watchdog is installed, it restarts DeviceMon.exe within 15 seconds. To fully stop, uninstall the watchdog first (`GameHost.exe --uninstall` as Admin), then close DeviceMon.
507+
A: If the watchdog is installed, it restarts DeviceMon.exe within ~5 seconds. To fully stop, uninstall the watchdog first (`GameHost.exe --uninstall` as Admin), then close DeviceMon.
466508

467509
**Q: Does the app work offline / without internet?**
468510
A: Yes. The dashboard and all monitoring features work entirely locally. Only email notifications/control require internet access.

RELEASE_NOTES_v1.05.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# Monitor & Control v1.05
2+
3+
## Highlights
4+
5+
- **Tamper-resistant data.** `monitor.db` and `events.log` now live in the
6+
SYSTEM-managed `C:\ProgramData\SystemHelper\` directory and are locked by the
7+
GameHost watchdog so a standard (child) user can no longer delete or rename
8+
them — closing DeviceMon no longer allows wiping usage history or logs.
9+
- **Automatic recovery.** The watchdog keeps SYSTEM-only backups under
10+
`C:\ProgramData\SystemHelper\Protected\` and restores the database (and
11+
`DeviceMon.exe`) if either goes missing.
12+
- **Faster relaunch.** The watchdog check interval was reduced from 15s to 5s,
13+
shrinking the window in which a manually closed monitor stays down.
14+
- **Protected install location.** New `install.ps1` installs DeviceMon into
15+
`C:\Program Files\DeviceMon`, where standard users have read-and-execute only -
16+
the child can close DeviceMon but cannot delete or modify the application files
17+
(`.exe`s, DLLs, `appsettings.json`). Re-run `install.ps1` as admin to update.
18+
- **Quiet SYSTEM updates for protected installs.** When a trusted update source is
19+
configured (`install.ps1 -UpdateSource ...`), the dashboard **Update** button
20+
queues a silent update performed by the `GameHost` service as SYSTEM - no UAC
21+
prompt, works remotely, and installs into the protected folder. The source is
22+
stored in a SYSTEM-only file the child cannot read or redirect, so a standard
23+
user cannot point updates at malicious files.
24+
- **Corrupt-config resilience.** A malformed `appsettings.json` no longer crashes
25+
DeviceMon or takes down the dashboard. Configuration now falls back to a
26+
last-known-good backup (and then built-in defaults), and the web host no longer
27+
aborts on an unparseable config file.
28+
29+
## Fixes and hardening
30+
31+
- Data files are protected with an explicit deny-delete ACL plus an `OWNER RIGHTS`
32+
cap, so the owning user cannot re-grant themselves delete permission.
33+
- "Clear log" now truncates the log in place instead of deleting the file, so it
34+
works alongside the deny-delete protection.
35+
- Executable and data restore are skipped while an update is in progress, so an
36+
update never fights the watchdog.
37+
- Uninstalling GameHost automatically releases the protection so an administrator
38+
can reset or manage the data.
39+
- A valid configuration is copied to a protected last-known-good file on every
40+
successful load, so a corrupted `appsettings.json` self-recovers the real
41+
settings instead of silently reverting to defaults.
42+
43+
## Upgrade notes
44+
45+
- For a child's PC, install with `install.ps1` (as Administrator) so the app lives
46+
in the protected `C:\Program Files\DeviceMon` folder. Update the same way -
47+
re-run `install.ps1`. The in-dashboard update flow only applies to non-protected
48+
installs, since it writes into the application folder.
49+
- Existing databases and logs are migrated automatically from the old
50+
`%LOCALAPPDATA%\SystemHelper\` location on first launch of this version.
51+
- Keep `DeviceMon.exe`, `GameHost.exe`, `PopupHost.exe`, and `UpdateAgent.exe`
52+
together when replacing an installation. The tamper protection only takes effect
53+
once the **new `GameHost.exe`** is in place — after updating, confirm the update
54+
log does not report `Skipped locked watchdog file: GameHost.exe`.
55+
- This framework-dependent package requires the .NET 8 Desktop Runtime on the
56+
target PC.
57+
58+
## Verification
59+
60+
- Release build completed successfully for all four executables.
61+
- Full regression suite passed (28 tests).
62+
- Deny-delete / owner-rights ACL verified against a live file.

0 commit comments

Comments
 (0)