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; + } + } + } +}