diff --git a/README.md b/README.md
index 934bc74..298a485 100644
--- a/README.md
+++ b/README.md
@@ -34,20 +34,22 @@ Website: [www.dpmworld.net](https://www.dpmworld.net)
### 🏢 Predefined Target Presets
Pre-populated with high-priority enterprise cloud endpoints and live certificate baselines:
1. **Citrix NetScaler Gateway Service** (`global-all.g.nssvc.net:443`) — *Citrix DaaS Control Plane*
-2. **Citrix Portal** (`citrix.com:443`) — *Official Citrix Portal*
-3. **Microsoft Entra ID / Azure AD** (`login.microsoftonline.com:443`) — *Cloud Authentication & SSO*
-4. **Azure Virtual Desktop Gateway** (`rdgateway.wvd.microsoft.com:443`) — *Microsoft AVD Gateway Service*
-5. **Microsoft Teams** (`teams.microsoft.com:443`) — *Microsoft Teams Web & Native Client*
-6. **Office 365 Exchange** (`outlook.office365.com:443`) — *Exchange Online Mail Services*
-7. **GitHub Services** (`github.com:443`) — *GitHub & Git over HTTPS (Sectigo/DigiCert/USERTrust)*
-8. **Amazon S3 API** (`s3.amazonaws.com:443`) — *AWS S3 Cloud Storage API*
-9. **Docker Hub Registry** (`registry-1.docker.io:443`) — *Container Image Registry*
-10. **Zoom Cloud Meetings** (`zoom.us:443`) — *Zoom Video Conferencing*
-11. **Google Public Web** (`google.com:443`) — *Google Public Edge (Google Trust Services)*
-12. **Cloudflare Public Edge** (`cloudflare.com:443`) — *Cloudflare CDN Edge*
-13. **BadSSL Expired (Test)** (`expired.badssl.com:443`) — *Diagnostic Expired Cert Test (COMODO/Let's Encrypt)*
-14. **BadSSL Self-Signed (Test)** (`self-signed.badssl.com:443`) — *Diagnostic Self-Signed Test*
-15. **BadSSL Untrusted Root (Test)** (`untrusted-root.badssl.com:443`) — *Diagnostic Untrusted Root Test*
+2. **Citrix Workspace Agent Hub (EU)** (`agenthub-eu.citrixworkspacesapi.net:443`) — *Citrix DaaS Agent Control Plane (Europe)*
+3. **Citrix Workspace Agent Hub (US)** (`agenthub-us.citrixworkspacesapi.net:443`) — *Citrix DaaS Agent Control Plane (United States)*
+4. **Citrix Workspace Agent Hub (AP-S)** (`agenthub-ap-s.citrixworkspacesapi.net:443`) — *Citrix DaaS Agent Control Plane (Asia-Pacific)*
+5. **Microsoft Entra ID / Azure AD** (`login.microsoftonline.com:443`) — *Cloud Authentication & SSO*
+6. **Azure Virtual Desktop Gateway** (`rdgateway.wvd.microsoft.com:443`) — *Microsoft AVD Gateway Service*
+7. **Microsoft Teams** (`teams.microsoft.com:443`) — *Microsoft Teams Web & Native Client*
+8. **Office 365 Exchange** (`outlook.office365.com:443`) — *Exchange Online Mail Services*
+9. **GitHub Services** (`github.com:443`) — *GitHub & Git over HTTPS (Sectigo/DigiCert/USERTrust)*
+10. **Amazon S3 API** (`s3.amazonaws.com:443`) — *AWS S3 Cloud Storage API*
+11. **Docker Hub Registry** (`registry-1.docker.io:443`) — *Container Image Registry*
+12. **Zoom Cloud Meetings** (`zoom.us:443`) — *Zoom Video Conferencing*
+13. **Google Public Web** (`google.com:443`) — *Google Public Edge (Google Trust Services)*
+14. **Cloudflare Public Edge** (`cloudflare.com:443`) — *Cloudflare CDN Edge*
+15. **BadSSL Expired (Test)** (`expired.badssl.com:443`) — *Diagnostic Expired Cert Test (COMODO/Let's Encrypt)*
+16. **BadSSL Self-Signed (Test)** (`self-signed.badssl.com:443`) — *Diagnostic Self-Signed Test*
+17. **BadSSL Untrusted Root (Test)** (`untrusted-root.badssl.com:443`) — *Diagnostic Untrusted Root Test*
### 🎨 User Experience, Reliability & Performance
- **Windows 11 Light Theme & Dynamic Auto-Sizing**: Clean Fluent Design with Windows 11 `ui:TitleBar` controls and dynamic column auto-sizing preventing string truncation in Italian, English, and French.
diff --git a/src/MITMPulse/MITMPulse.csproj b/src/MITMPulse/MITMPulse.csproj
index c495435..776133d 100644
--- a/src/MITMPulse/MITMPulse.csproj
+++ b/src/MITMPulse/MITMPulse.csproj
@@ -13,9 +13,9 @@
Francesco Dipietromaria
dpmworld.net
Copyright © 2026 Francesco Dipietromaria (www.dpmworld.net)
- 1.0.21
- 1.0.21.0
- 1.0.21.0
+ 1.0.24
+ 1.0.24.0
+ 1.0.24.0
Assets\icon.ico
diff --git a/src/MITMPulse/MainWindow.xaml b/src/MITMPulse/MainWindow.xaml
index 4c27036..8b840c7 100644
--- a/src/MITMPulse/MainWindow.xaml
+++ b/src/MITMPulse/MainWindow.xaml
@@ -72,8 +72,8 @@
-
+
diff --git a/src/MITMPulse/MainWindow.xaml.cs b/src/MITMPulse/MainWindow.xaml.cs
index 85c0586..1dd6dbc 100644
--- a/src/MITMPulse/MainWindow.xaml.cs
+++ b/src/MITMPulse/MainWindow.xaml.cs
@@ -8,5 +8,11 @@ public MainWindow()
{
InitializeComponent();
DataContext = App.ViewModel;
+ Loaded += MainWindow_Loaded;
+ }
+
+ private void MainWindow_Loaded(object sender, System.Windows.RoutedEventArgs e)
+ {
+ App.ViewModel?.CheckFirstLaunchDisclaimer();
}
}
\ No newline at end of file
diff --git a/src/MITMPulse/Models/AppSettings.cs b/src/MITMPulse/Models/AppSettings.cs
index d990368..37b6888 100644
--- a/src/MITMPulse/Models/AppSettings.cs
+++ b/src/MITMPulse/Models/AppSettings.cs
@@ -6,9 +6,10 @@ namespace MITMPulse.Models;
public class AppSettings
{
public string LanguageCode { get; set; } = "auto";
- public ProxyMode SelectedProxyMode { get; set; } = ProxyMode.Direct;
+ public ProxyMode SelectedProxyMode { get; set; } = ProxyMode.System;
public string CustomProxyHost { get; set; } = string.Empty;
public int CustomProxyPort { get; set; } = 8080;
+ public DateTime? DisclaimerAckDate { get; set; }
}
public record LanguageOption(string Code, string DisplayName)
diff --git a/src/MITMPulse/ViewModels/MainViewModel.cs b/src/MITMPulse/ViewModels/MainViewModel.cs
index 1f9b55e..f3ebff6 100644
--- a/src/MITMPulse/ViewModels/MainViewModel.cs
+++ b/src/MITMPulse/ViewModels/MainViewModel.cs
@@ -42,7 +42,9 @@ public string AppTitleWithVersion
private string _expectedThumbprint = string.Empty;
[ObservableProperty]
- private ProxyMode _selectedProxyMode = ProxyMode.Direct;
+ private ProxyMode _selectedProxyMode = ProxyMode.System;
+
+ private DateTime? _disclaimerAckDate;
[ObservableProperty]
private string _customProxyHost = string.Empty;
@@ -102,12 +104,30 @@ public MainViewModel(
SelectedProxyMode = initialSettings.SelectedProxyMode;
CustomProxyHost = initialSettings.CustomProxyHost;
CustomProxyPort = initialSettings.CustomProxyPort;
+ _disclaimerAckDate = initialSettings.DisclaimerAckDate;
}
InitializePresets();
_ = LoadHistoryAsync();
}
+ public void CheckFirstLaunchDisclaimer()
+ {
+ if (_disclaimerAckDate.HasValue) return;
+
+ string title = LocalizedStrings.Instance["DisclaimerTitle"];
+ string text = LocalizedStrings.Instance["DisclaimerText"];
+
+ System.Windows.MessageBox.Show(
+ text,
+ title,
+ System.Windows.MessageBoxButton.OK,
+ System.Windows.MessageBoxImage.Information);
+
+ _disclaimerAckDate = DateTime.UtcNow;
+ _ = SaveSettingsAsync();
+ }
+
private void InitializeLanguages(string initialCode)
{
AvailableLanguages = new ObservableCollection
@@ -145,13 +165,31 @@ private void InitializePresets()
"FD93956E6143F0942FC4FD6760E65BE4FC4C8F19"),
new EndpointPreset(
- "Citrix Portal",
- "citrix.com",
+ "Citrix Workspace Agent Hub (EU)",
+ "agenthub-eu.citrixworkspacesapi.net",
443,
- "Citrix Official Web Portal",
- new[] { "DigiCert", "Sectigo", "GoDaddy" },
- new DateTime(2027, 2, 11, 0, 59, 59, DateTimeKind.Utc),
- "E25DB037FE08E8D08A280E06276536C5AA0481AE"),
+ "Citrix Workspace Agent Control Plane (Europe)",
+ new[] { "DigiCert", "Sectigo" },
+ new DateTime(2027, 1, 11, 0, 59, 59, DateTimeKind.Utc),
+ "2EC3B62283D0BD480A2D5777E33CED1D88215778"),
+
+ new EndpointPreset(
+ "Citrix Workspace Agent Hub (US)",
+ "agenthub-us.citrixworkspacesapi.net",
+ 443,
+ "Citrix Workspace Agent Control Plane (United States)",
+ new[] { "DigiCert", "Sectigo" },
+ new DateTime(2027, 1, 11, 0, 59, 59, DateTimeKind.Utc),
+ "0569B9577FE6C1C952B3B1DB48C0D9A75EB4384F"),
+
+ new EndpointPreset(
+ "Citrix Workspace Agent Hub (AP-S)",
+ "agenthub-ap-s.citrixworkspacesapi.net",
+ 443,
+ "Citrix Workspace Agent Control Plane (Asia-Pacific)",
+ new[] { "DigiCert", "Sectigo" },
+ new DateTime(2027, 1, 11, 0, 59, 59, DateTimeKind.Utc),
+ "86892F1A8CEB820BF5D407C7C5BB91EAF30AAC1C"),
new EndpointPreset(
"Microsoft Entra ID / Azure AD",
@@ -160,7 +198,7 @@ private void InitializePresets()
"Microsoft Authentication Service (High SSO SSL Inspection Risk)",
new[] { "DigiCert", "Microsoft", "Sectigo", "GlobalSign" },
new DateTime(2026, 12, 12, 0, 59, 59, DateTimeKind.Utc),
- "EB7711B2EA1A8D920C5060328A08A24A93C288DE"),
+ "EB7711B2EA1A8D920C5060328A08A24A93C288DE;D36EDAAC39C9581AE2F36F16D9E2C03DB5C7DADB"),
new EndpointPreset(
"Azure Virtual Desktop Gateway",
@@ -169,7 +207,7 @@ private void InitializePresets()
"Microsoft Remote Desktop / AVD Gateway Service",
new[] { "Microsoft", "DigiCert", "Sectigo" },
new DateTime(2027, 1, 19, 17, 31, 41, DateTimeKind.Utc),
- "2F5DA3E7FA20B155C0B5E234A3152A6E4D99B6B3;B80140DC2DD174B577C81D9E30B718090CD166FC"),
+ "2F5DA3E7FA20B155C0B5E234A3152A6E4D99B6B3;B80140DC2DD174B577C81D9E30B718090CD166FC;0DDB05F2F8EC6D2B8DB60C6B8A5AE196D1C40B40"),
new EndpointPreset(
"Microsoft Teams",
@@ -421,7 +459,8 @@ private async Task SaveSettingsAsync()
LanguageCode = SelectedLanguage?.Code ?? "auto",
SelectedProxyMode = SelectedProxyMode,
CustomProxyHost = CustomProxyHost,
- CustomProxyPort = CustomProxyPort
+ CustomProxyPort = CustomProxyPort,
+ DisclaimerAckDate = _disclaimerAckDate
};
await _settingsService.SaveSettingsAsync(settings);
}
diff --git a/tests/MITMPulse.Tests/Services/PresetVerificationTests.cs b/tests/MITMPulse.Tests/Services/PresetVerificationTests.cs
new file mode 100644
index 0000000..737f595
--- /dev/null
+++ b/tests/MITMPulse.Tests/Services/PresetVerificationTests.cs
@@ -0,0 +1,126 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.Security;
+using System.Net.Sockets;
+using System.Security.Cryptography.X509Certificates;
+using System.Threading;
+using System.Threading.Tasks;
+using Moq;
+using MITMPulse.Models;
+using MITMPulse.Services;
+using MITMPulse.ViewModels;
+using Xunit;
+
+namespace MITMPulse.Tests.Services;
+
+public class PresetVerificationTests
+{
+ [Fact]
+ public async Task Presets_VerifyOnlineThumbprintsAndExpirations()
+ {
+ // Arrange: Instantiate ViewModel with mocked services to load presets
+ var mockSsl = new Mock();
+ var mockProxy = new Mock();
+ var mockHistory = new Mock();
+ mockHistory.Setup(h => h.GetHistoryAsync(It.IsAny()))
+ .ReturnsAsync(new List());
+
+ var vm = new MainViewModel(mockSsl.Object, mockProxy.Object, mockHistory.Object);
+ var activePresets = vm.Presets
+ .Where(p => !p.Host.Contains("badssl.com"))
+ .ToList();
+
+ // Act: Run certificate validation in parallel
+ var tasks = activePresets.Select(preset => VerifyPresetAsync(preset));
+ var results = await Task.WhenAll(tasks);
+
+ // Assert: Verify all presets passed without failures
+ var failures = results.Where(r => !string.IsNullOrEmpty(r)).ToList();
+ if (failures.Count > 0)
+ {
+ var aggregatedMessage = "Preset verification failed for some endpoints:\n" + string.Join("\n", failures);
+ Assert.Fail(aggregatedMessage);
+ }
+ }
+
+ private async Task VerifyPresetAsync(EndpointPreset preset)
+ {
+ using var tcpClient = new TcpClient();
+ using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
+
+ try
+ {
+ await tcpClient.ConnectAsync(preset.Host, preset.Port, cts.Token);
+ using var stream = tcpClient.GetStream();
+ using var sslStream = new SslStream(stream, false, (sender, certificate, chain, errors) => true);
+ await sslStream.AuthenticateAsClientAsync(new SslClientAuthenticationOptions
+ {
+ TargetHost = preset.Host,
+ EnabledSslProtocols = System.Security.Authentication.SslProtocols.Tls12 | System.Security.Authentication.SslProtocols.Tls13
+ }, cts.Token);
+
+ var remoteCert = sslStream.RemoteCertificate;
+ if (remoteCert == null)
+ {
+ return $"[{preset.DisplayName}] Remote server returned a null certificate.";
+ }
+
+ using var cert2 = new X509Certificate2(remoteCert);
+
+ // Validate Thumbprint (must match one of the expected list)
+ string actualThumbprint = cert2.Thumbprint.ToUpperInvariant();
+ if (string.IsNullOrWhiteSpace(preset.KnownThumbprint))
+ {
+ return $"[{preset.DisplayName}] KnownThumbprint is not configured in the preset.";
+ }
+
+ var expectedThumbprints = preset.KnownThumbprint
+ .Split(new[] { ',', ';', '|' }, StringSplitOptions.RemoveEmptyEntries)
+ .Select(p => p.Replace(" ", "").Replace(":", "").ToUpperInvariant())
+ .ToList();
+
+ if (!expectedThumbprints.Contains(actualThumbprint))
+ {
+ var hostLower = preset.Host.ToLowerInvariant();
+ bool isDynamicPublicEndpoint = hostLower.Contains("microsoft") ||
+ hostLower.Contains("google") ||
+ hostLower.Contains("cloudflare") ||
+ hostLower.Contains("docker");
+
+ if (isDynamicPublicEndpoint)
+ {
+ Console.WriteLine($"[Dynamic Warning] [{preset.DisplayName}] Thumbprint rotated online. Expected: {preset.KnownThumbprint}, Actual online: {actualThumbprint}");
+ return null;
+ }
+
+ return $"[{preset.DisplayName}] Thumbprint MISMATCH! Expected: {preset.KnownThumbprint}, Actual online: {actualThumbprint}";
+ }
+
+ // Validate Expiration Date (online expiration must not be past the known date, i.e., cert is valid)
+ if (preset.KnownCertExpirationDate.HasValue)
+ {
+ if (DateTime.UtcNow > cert2.NotAfter)
+ {
+ return $"[{preset.DisplayName}] Certificate has EXPIRED online! (NotAfter: {cert2.NotAfter:yyyy-MM-dd})";
+ }
+ }
+
+ return null;
+ }
+ catch (Exception ex)
+ {
+ bool isCiCd = Environment.GetEnvironmentVariable("GITHUB_ACTIONS") == "true";
+ if (isCiCd)
+ {
+ return $"[{preset.DisplayName}] Connection/SSL Error: {ex.Message}";
+ }
+ else
+ {
+ // Locally, log the warning but do not break the test run (offline or proxy-only environment support)
+ Console.WriteLine($"[Local Warning] [{preset.DisplayName}] Connection skipped/failed: {ex.Message}");
+ return null;
+ }
+ }
+ }
+}