Skip to content

Commit e8f6767

Browse files
orstrocursoragent
andcommitted
Chrome-like UI, ForensicEngine/PrivacyEngine improvements, time consistency
Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent ebbeb0a commit e8f6767

10 files changed

Lines changed: 1145 additions & 566 deletions

‎BrowserTab.cs‎

Lines changed: 99 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,15 @@
66

77
namespace PrivacyMonitor
88
{
9+
/// <summary>Per-tab state: UI refs, request/fingerprint data, and protection state. UI refs are set by MainWindow after creation.
10+
/// BlockedRequests and other lists are not thread-safe; RegisterBlocked and DrainPending must be called on the UI thread.
11+
/// Consider constructor injection for required UI refs when refactoring the creation flow.</summary>
912
public class BrowserTab
1013
{
1114
public string Id { get; } = Guid.NewGuid().ToString("N")[..8];
15+
/// <summary>Set by MainWindow after creation. Not null once tab is fully initialized.</summary>
1216
public WebView2 WebView { get; set; } = null!;
17+
/// <summary>Set by MainWindow after BuildTabHeader(tab).</summary>
1318
public Border TabHeader { get; set; } = null!;
1419
public TextBlock TitleBlock { get; set; } = null!;
1520
public TextBlock InitialBlock { get; set; } = null!;
@@ -20,11 +25,13 @@ public class BrowserTab
2025
public string Title { get; set; } = "New Tab";
2126
public string Url { get; set; } = "";
2227
public string CurrentHost { get; set; } = "";
23-
public DateTime ScanStart { get; set; } = DateTime.Now;
28+
/// <summary>When the current page scan started (UTC). Use UTC for consistency with LastActivityUtc.</summary>
29+
public DateTime ScanStart { get; set; } = DateTime.UtcNow;
2430
/// <summary>Last time this tab had user or network activity (UTC). Used for idle sleep.</summary>
2531
public DateTime LastActivityUtc { get; set; } = DateTime.UtcNow;
2632
/// <summary>Last measured approximate memory usage for this tab (bytes, from JS APIs).</summary>
2733
public long? LastMemoryBytes { get; set; }
34+
/// <summary>Next ID for new requests. Incremented at ingestion (e.g. Interlocked.Increment in the request handler).</summary>
2835
public int NextRequestId;
2936
public bool IsReady { get; set; }
3037
public bool IsLoading { get; set; }
@@ -38,17 +45,25 @@ public class BrowserTab
3845

3946
// ── Thread-safe request ingestion ──
4047
public ConcurrentQueue<RequestEntry> PendingRequests { get; } = new();
48+
/// <summary>Max requests kept per tab. Consider making configurable via settings.</summary>
4149
public const int MaxRequests = 5000;
50+
/// <summary>Max blocked-request records kept. Consider making configurable.</summary>
4251
public const int MaxBlockedRequests = 2000;
4352

44-
// ── Processed data (UI-thread only) ──
45-
public List<RequestEntry> Requests { get; } = new();
53+
// ── Processed data (UI-thread only; not thread-safe) ──
54+
/// <summary>LinkedList for O(1) eviction when over MaxRequests; use Last/Previous to scan recent-first.</summary>
55+
public LinkedList<RequestEntry> Requests { get; } = new();
4656
public List<FingerprintFinding> Fingerprints { get; } = new();
4757
public List<CookieItem> Cookies { get; } = new();
4858
public List<StorageItem> Storage { get; } = new();
4959
public List<WebRtcLeak> WebRtcLeaks { get; } = new();
5060
public List<SecurityHeaderResult> SecurityHeaders { get; set; } = new();
5161

62+
/// <summary>Raised after one or more requests are drained (UI thread). Optional for UI refresh. Subscribers doing heavy work may want to debounce.</summary>
63+
public event Action? RequestProcessed;
64+
/// <summary>Raised when a request is recorded as blocked. Must only be called from the UI thread (BlockedRequests is not thread-safe).</summary>
65+
public event Action<BlockedRequestInfo>? BlockedRequestAdded;
66+
5267
// ── Detection context (for cross-request correlation) ──
5368
public HashSet<string> SeenTrackerDomains { get; } = new(StringComparer.OrdinalIgnoreCase);
5469
public HashSet<string> SeenTrackerCompanies { get; } = new(StringComparer.OrdinalIgnoreCase);
@@ -58,6 +73,8 @@ public class BrowserTab
5873
// ── Forensic state ──
5974
public Dictionary<string, HashSet<string>> IdentifierToDomains { get; } = new();
6075
public List<DataFlowEdge> DataFlowEdges { get; } = new();
76+
/// <summary>O(1) lookup for AddOrUpdateEdge; cleared with DataFlowEdges on ResetDetection.</summary>
77+
public Dictionary<string, DataFlowEdge> DataFlowEdgeLookup { get; } = new();
6178
public List<ForensicEvent> ForensicTimeline { get; } = new();
6279

6380
// ── Protection state ──
@@ -69,49 +86,107 @@ public class BrowserTab
6986
public string? FingerprintDetectScriptId { get; set; }
7087
public string? BehavioralMonitorScriptId { get; set; }
7188

72-
/// <summary>Drain pending queue into Requests list. Call from UI thread.</summary>
89+
/// <summary>Drain pending queue into Requests. Call from UI thread only. Eviction is O(1) via LinkedList.</summary>
7390
public int DrainPending()
7491
{
7592
int drained = 0;
7693
while (PendingRequests.TryDequeue(out var entry))
7794
{
78-
Requests.Add(entry);
95+
Requests.AddLast(entry);
7996
drained++;
8097

81-
// Track domain counts
82-
DomainRequestCounts[entry.Host] = DomainRequestCounts.GetValueOrDefault(entry.Host) + 1;
98+
var host = entry.Host ?? "";
99+
DomainRequestCounts[host] = DomainRequestCounts.GetValueOrDefault(host) + 1;
83100

84-
// Track seen trackers
85-
if (!string.IsNullOrEmpty(entry.TrackerLabel))
101+
if (!string.IsNullOrEmpty(entry.TrackerLabel) && host.Length > 0)
86102
{
87-
SeenTrackerDomains.Add(entry.Host);
103+
SeenTrackerDomains.Add(host);
88104
if (!string.IsNullOrEmpty(entry.TrackerCompany))
89105
SeenTrackerCompanies.Add(entry.TrackerCompany);
90106
}
91107

92-
// Forensic correlation (runs on UI thread, lightweight per-request)
93-
ForensicEngine.ExtractAndRegisterIdentifiers(entry, IdentifierToDomains, ForensicTimeline);
94-
ForensicEngine.AnalyzeDataFlow(entry, CurrentHost, DataFlowEdges, ForensicTimeline);
108+
try
109+
{
110+
ForensicEngine.ExtractAndRegisterIdentifiers(entry, IdentifierToDomains, ForensicTimeline);
111+
ForensicEngine.AnalyzeDataFlow(entry, CurrentHost, DataFlowEdges, ForensicTimeline, DataFlowEdgeLookup);
112+
}
113+
catch
114+
{
115+
// ForensicEngine failed; avoid breaking drain. Log in host app if needed.
116+
}
95117

96-
// Evict oldest if over limit
97118
if (Requests.Count > MaxRequests)
98-
Requests.RemoveAt(0);
119+
Requests.RemoveFirst();
99120
}
121+
if (drained > 0)
122+
RequestProcessed?.Invoke();
100123
return drained;
101124
}
102125

103-
/// <summary>Reset all detection state (on host change).</summary>
126+
/// <summary>Reset all detection state (on host change). Preserves list references for bindings (e.g. SecurityHeaders.Clear()).</summary>
104127
public void ResetDetection()
105128
{
106-
Requests.Clear(); Fingerprints.Clear(); Cookies.Clear();
107-
Storage.Clear(); WebRtcLeaks.Clear(); SecurityHeaders = new();
108-
NextRequestId = 0; ScanStart = DateTime.Now; ConsentDetected = false;
109-
SeenTrackerDomains.Clear(); SeenTrackerCompanies.Clear();
110-
DomainRequestCounts.Clear(); SeenUrls.Clear();
111-
IdentifierToDomains.Clear(); DataFlowEdges.Clear(); ForensicTimeline.Clear();
112-
BlockedCount = 0; BlockedRequests.Clear(); AntiFingerprintInjected = false;
113-
// Drain any pending
129+
Requests.Clear();
130+
Fingerprints.Clear();
131+
Cookies.Clear();
132+
Storage.Clear();
133+
WebRtcLeaks.Clear();
134+
SecurityHeaders.Clear();
135+
NextRequestId = 0;
136+
ScanStart = DateTime.UtcNow;
137+
ConsentDetected = false;
138+
SeenTrackerDomains.Clear();
139+
SeenTrackerCompanies.Clear();
140+
DomainRequestCounts.Clear();
141+
SeenUrls.Clear();
142+
IdentifierToDomains.Clear();
143+
DataFlowEdges.Clear();
144+
DataFlowEdgeLookup.Clear();
145+
ForensicTimeline.Clear();
146+
BlockedCount = 0;
147+
BlockedRequests.Clear();
148+
AntiFingerprintInjected = false;
114149
while (PendingRequests.TryDequeue(out _)) { }
115150
}
151+
152+
/// <summary>Record user or network activity (updates LastActivityUtc for idle sleep).</summary>
153+
public void NoteActivity()
154+
{
155+
LastActivityUtc = DateTime.UtcNow;
156+
}
157+
158+
/// <summary>Mark this tab as crashed (e.g. after ProcessFailed).</summary>
159+
public void MarkCrashed()
160+
{
161+
IsCrashed = true;
162+
}
163+
164+
/// <summary>Mark this tab as sleeping (suspended) or awake.</summary>
165+
public void MarkSleeping(bool sleeping = true)
166+
{
167+
IsSleeping = sleeping;
168+
}
169+
170+
/// <summary>Record a blocked request for the forensic trail and badge. Must be called on the UI thread (BlockedRequests is not thread-safe).</summary>
171+
public void RegisterBlocked(DateTime time, string host, string url, string reason, string category, double confidence, string trackerLabel, string resourceType, string method)
172+
{
173+
BlockedCount++;
174+
var info = new BlockedRequestInfo
175+
{
176+
Time = time,
177+
Host = host ?? "",
178+
Url = url ?? "",
179+
Reason = reason ?? "",
180+
Category = category ?? "",
181+
Confidence = confidence,
182+
TrackerLabel = trackerLabel ?? "",
183+
ResourceType = resourceType ?? "",
184+
Method = method ?? ""
185+
};
186+
BlockedRequests.Add(info);
187+
if (BlockedRequests.Count > MaxBlockedRequests)
188+
BlockedRequests.RemoveAt(0);
189+
BlockedRequestAdded?.Invoke(info);
190+
}
116191
}
117192
}

‎DesktopShortcut.cs‎

Lines changed: 77 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
using System.IO;
22
using System.Runtime.InteropServices;
3+
using System;
4+
using System.Threading.Tasks;
35

46
namespace PrivacyMonitor;
57

68
/// <summary>
79
/// Creates a Windows desktop shortcut to the current app so users have easy access after download/install.
10+
/// Reflection-based COM interop is used for compatibility and to avoid an extra dependency.
811
/// </summary>
912
public static class DesktopShortcut
1013
{
@@ -13,11 +16,21 @@ public static class DesktopShortcut
1316
/// <summary>
1417
/// Ensures a desktop shortcut exists. Creates it if missing or if the target path changed (e.g. user moved the exe).
1518
/// Call once at app startup (e.g. from App.OnStartup). Safe to call every run; only creates/updates when needed.
19+
/// For UI snappiness, consider running in a background task if startup time is critical.
1620
/// </summary>
1721
public static void EnsureDesktopShortcut()
22+
{
23+
// Note: Shortcut creation is very fast but can be put on a background thread if desired:
24+
// Task.Run(() => EnsureDesktopShortcutImpl());
25+
26+
EnsureDesktopShortcutImpl();
27+
}
28+
29+
private static void EnsureDesktopShortcutImpl()
1830
{
1931
try
2032
{
33+
// .NET 6+ ProcessPath; fallback just in case
2134
var exePath = Environment.ProcessPath ?? Path.Combine(AppContext.BaseDirectory, "PrivacyMonitor.exe");
2235
if (string.IsNullOrEmpty(exePath) || !File.Exists(exePath))
2336
return;
@@ -30,78 +43,115 @@ public static void EnsureDesktopShortcut()
3043
var exeDir = Path.GetDirectoryName(exePath) ?? "";
3144

3245
// If shortcut exists and already points to this exe, skip
33-
if (File.Exists(shortcutPath) && GetShortcutTargetPath(shortcutPath) == exePath)
46+
var targetOfExisting = File.Exists(shortcutPath) ? GetShortcutTargetPath(shortcutPath) : null;
47+
if (targetOfExisting != null && string.Equals(targetOfExisting, exePath, StringComparison.OrdinalIgnoreCase))
3448
return;
3549

3650
CreateShortcut(shortcutPath, exePath, exeDir);
3751
}
38-
catch
52+
catch (Exception ex)
3953
{
4054
// Non-critical: do not crash the app if shortcut creation fails (e.g. no desktop, permissions).
55+
// Optionally log here for diagnostics.
56+
// Console.Error.WriteLine($"Failed to ensure desktop shortcut: {ex}");
4157
}
4258
}
4359

60+
/// <summary>
61+
/// Creates a .lnk shortcut using late-bound COM interop.
62+
/// </summary>
4463
private static void CreateShortcut(string shortcutPath, string targetPath, string workingDirectory)
4564
{
46-
Type? shellType = Type.GetTypeFromProgID("WScript.Shell");
65+
Type? shellType = Type.GetTypeFromProgID("WScript.Shell", throwOnError: false);
4766
if (shellType == null)
4867
return;
4968

50-
object? shell = Activator.CreateInstance(shellType);
51-
if (shell == null)
52-
return;
53-
69+
object? shell = null;
70+
object? shortcut = null;
5471
try
5572
{
56-
// CreateShortcut(fullPath) returns IShellLinkW / IWshShortcut
57-
object? shortcut = shellType.InvokeMember("CreateShortcut", System.Reflection.BindingFlags.InvokeMethod, null, shell, new object[] { shortcutPath });
73+
shell = Activator.CreateInstance(shellType);
74+
if (shell == null)
75+
return;
76+
77+
// CreateShortcut returns a COM object implementing IWshShortcut
78+
shortcut = shellType.InvokeMember(
79+
"CreateShortcut",
80+
System.Reflection.BindingFlags.InvokeMethod,
81+
null,
82+
shell,
83+
new object[] { shortcutPath });
84+
5885
if (shortcut == null)
5986
return;
6087

6188
var t = shortcut.GetType();
89+
// Reflection slightly slower than direct, but fine for infrequent calls.
6290
t.InvokeMember("TargetPath", System.Reflection.BindingFlags.SetProperty, null, shortcut, new object[] { targetPath });
6391
t.InvokeMember("WorkingDirectory", System.Reflection.BindingFlags.SetProperty, null, shortcut, new object[] { workingDirectory });
6492
t.InvokeMember("Description", System.Reflection.BindingFlags.SetProperty, null, shortcut, new object[] { "Privacy Monitor – Privacy-first browser" });
93+
// Save shortcut to disk
6594
t.InvokeMember("Save", System.Reflection.BindingFlags.InvokeMethod, null, shortcut, Array.Empty<object>());
6695
}
6796
finally
6897
{
98+
// Release COM objects, safe for both regular and exceptional paths.
99+
if (shortcut != null && Marshal.IsComObject(shortcut))
100+
Marshal.ReleaseComObject(shortcut);
101+
69102
if (shell is IDisposable d)
70103
d.Dispose();
71-
else if (Marshal.IsComObject(shell))
104+
else if (shell != null && Marshal.IsComObject(shell))
72105
Marshal.ReleaseComObject(shell);
73106
}
74107
}
75108

109+
/// <summary>
110+
/// Gets the TargetPath of a shortcut (.lnk file), or null on error.
111+
/// </summary>
76112
private static string? GetShortcutTargetPath(string shortcutPath)
77113
{
114+
Type? shellType = null;
115+
object? shell = null;
116+
object? shortcut = null;
78117
try
79118
{
80-
var shellType = Type.GetTypeFromProgID("WScript.Shell");
119+
shellType = Type.GetTypeFromProgID("WScript.Shell", throwOnError: false);
81120
if (shellType == null) return null;
82121

83-
object? shell = Activator.CreateInstance(shellType);
122+
shell = Activator.CreateInstance(shellType);
84123
if (shell == null) return null;
85124

86-
try
87-
{
88-
object? shortcut = shellType.InvokeMember("CreateShortcut", System.Reflection.BindingFlags.InvokeMethod, null, shell, new object[] { shortcutPath });
89-
if (shortcut == null) return null;
90-
91-
var target = shortcut.GetType().InvokeMember("TargetPath", System.Reflection.BindingFlags.GetProperty, null, shortcut, null);
92-
return target as string;
93-
}
94-
finally
95-
{
96-
if (shell is IDisposable d)
97-
d.Dispose();
98-
else if (Marshal.IsComObject(shell))
99-
Marshal.ReleaseComObject(shell);
100-
}
125+
shortcut = shellType.InvokeMember(
126+
"CreateShortcut",
127+
System.Reflection.BindingFlags.InvokeMethod,
128+
null,
129+
shell,
130+
new object[] { shortcutPath });
131+
if (shortcut == null) return null;
132+
133+
var target = shortcut.GetType().InvokeMember(
134+
"TargetPath",
135+
System.Reflection.BindingFlags.GetProperty,
136+
null,
137+
shortcut,
138+
null);
139+
return target as string;
101140
}
102141
catch
103142
{
143+
// Not critical: just means shortcut isn't valid or COM not available
104144
return null;
105145
}
146+
finally
147+
{
148+
if (shortcut != null && Marshal.IsComObject(shortcut))
149+
Marshal.ReleaseComObject(shortcut);
150+
151+
if (shell is IDisposable d)
152+
d.Dispose();
153+
else if (shell != null && Marshal.IsComObject(shell))
154+
Marshal.ReleaseComObject(shell);
155+
}
106156
}
107157
}

0 commit comments

Comments
 (0)