Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 16 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
6 changes: 3 additions & 3 deletions src/MITMPulse/MITMPulse.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@
<Authors>Francesco Dipietromaria</Authors>
<Company>dpmworld.net</Company>
<Copyright>Copyright © 2026 Francesco Dipietromaria (www.dpmworld.net)</Copyright>
<Version>1.0.21</Version>
<AssemblyVersion>1.0.21.0</AssemblyVersion>
<FileVersion>1.0.21.0</FileVersion>
<Version>1.0.24</Version>
<AssemblyVersion>1.0.24.0</AssemblyVersion>
<FileVersion>1.0.24.0</FileVersion>

<!-- App Branding -->
<ApplicationIcon>Assets\icon.ico</ApplicationIcon>
Expand Down
2 changes: 1 addition & 1 deletion src/MITMPulse/MainWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,8 @@
<StackPanel Grid.Row="0" Grid.Column="2" Margin="0,0,12,8">
<TextBlock Text="{Binding Source={x:Static services:LocalizedStrings.Instance}, Path=[ProxyModeLabel]}" FontSize="12" FontWeight="SemiBold" Margin="0,0,0,4"/>
<ComboBox SelectedValue="{Binding SelectedProxyMode}" SelectedValuePath="Tag">
<ComboBoxItem Content="Direct" Tag="{x:Static models:ProxyMode.Direct}"/>
<ComboBoxItem Content="System (WinINet)" Tag="{x:Static models:ProxyMode.System}"/>
<ComboBoxItem Content="Direct" Tag="{x:Static models:ProxyMode.Direct}"/>
<ComboBoxItem Content="WinHTTP (netsh)" Tag="{x:Static models:ProxyMode.WinHttp}"/>
<ComboBoxItem Content="Custom" Tag="{x:Static models:ProxyMode.Custom}"/>
</ComboBox>
Expand Down
6 changes: 6 additions & 0 deletions src/MITMPulse/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
3 changes: 2 additions & 1 deletion src/MITMPulse/Models/AppSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
59 changes: 49 additions & 10 deletions src/MITMPulse/ViewModels/MainViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<LanguageOption>
Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -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);
}
Expand Down
126 changes: 126 additions & 0 deletions tests/MITMPulse.Tests/Services/PresetVerificationTests.cs
Original file line number Diff line number Diff line change
@@ -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<ISslInspectionService>();
var mockProxy = new Mock<IProxyService>();
var mockHistory = new Mock<IHistoryService>();
mockHistory.Setup(h => h.GetHistoryAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<InspectionHistoryEntry>());

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<string?> 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;
}
}
}
}
Loading