namespace Omika.Tests.Api; /// /// Tests de sécurité du reverse proxy Apache local : analyse la source /// d'Omika.Api (Program.cs + ForwardedHeadersConfiguration.cs) pour garantir /// l'ordre du middleware, l'absence de configuration « trust-all », l'absence /// de certificat Kestrel et l'absence de bind sur 0.0.0.0. /// public class ReverseProxySecurityTests { private const string ProgramFile = "src/Omika.Api/Program.cs"; private const string ForwardedHeadersFile = "src/Omika.Api/ForwardedHeadersConfiguration.cs"; [Fact] public void Program_UsesForwardedHeadersBeforeHttpsRedirection() { string source = ReadRepoFile(ProgramFile); int forwarded = source.IndexOf("app.UseForwardedHeaders(", StringComparison.Ordinal); int https = source.IndexOf("app.UseHttpsRedirection(", StringComparison.Ordinal); Assert.True(forwarded >= 0, "UseForwardedHeaders doit être présent."); Assert.True(https >= 0, "UseHttpsRedirection doit être présent."); Assert.True( forwarded < https, "UseForwardedHeaders doit s'exécuter AVANT UseHttpsRedirection."); } [Fact] public void ReverseProxy_DoesNotClearProxyTrustLists() { string combined = ReadRepoFile(ProgramFile) + "\n" + ReadRepoFile(ForwardedHeadersFile); Assert.DoesNotContain("KnownNetworks.Clear", combined, StringComparison.Ordinal); Assert.DoesNotContain("KnownIPNetworks.Clear", combined, StringComparison.Ordinal); Assert.DoesNotContain("KnownProxies.Clear", combined, StringComparison.Ordinal); Assert.DoesNotContain("RequireHeaderSymmetry = false", combined, StringComparison.Ordinal); } [Fact] public void ReverseProxy_DoesNotAddWildcardProxyTrust() { string combined = ReadRepoFile(ProgramFile) + "\n" + ReadRepoFile(ForwardedHeadersFile); // Aucune adresse « any » (0.0.0.0 / [::]) ni réseau wildcard dans la confiance. Assert.DoesNotContain("IPAddress.Any", combined, StringComparison.Ordinal); Assert.DoesNotContain("IPAddress.IPv6Any", combined, StringComparison.Ordinal); Assert.DoesNotContain("0.0.0.0", combined, StringComparison.Ordinal); } [Fact] public void OmikaApi_DoesNotConfigureKestrelCertificate() { foreach (string file in EnumerateOmikaApiSourceFiles()) { string content = File.ReadAllText(file); // Pas de terminaison TLS dans Kestrel (certificat / HTTPS) : // c'est Apache qui termine TLS. Assert.DoesNotContain("UseHttps(", content, StringComparison.Ordinal); Assert.DoesNotContain("AddHttps", content, StringComparison.Ordinal); Assert.DoesNotContain("X509Certificate", content, StringComparison.Ordinal); Assert.DoesNotContain(".pfx", content, StringComparison.OrdinalIgnoreCase); Assert.DoesNotContain("HttpsConnectionAdapterOptions", content, StringComparison.Ordinal); } } [Fact] public void OmikaApi_DoesNotBindToAnyAddress() { foreach (string file in EnumerateOmikaApiSourceFiles()) { string content = File.ReadAllText(file); Assert.DoesNotContain("0.0.0.0", content, StringComparison.Ordinal); Assert.DoesNotContain("IPAddress.Any", content, StringComparison.Ordinal); Assert.DoesNotContain("IPAddress.IPv6Any", content, StringComparison.Ordinal); Assert.DoesNotContain("ListenAnyIP", content, StringComparison.Ordinal); } } [Fact] public void Program_DoesNotHardcodeKestrelPort() { string source = ReadRepoFile(ProgramFile); // Le port (5080) et l'URL doivent rester dans appsettings.Production.json, // jamais en dur dans Program.cs. Assert.DoesNotContain("5080", source, StringComparison.Ordinal); Assert.DoesNotContain("UseUrls", source, StringComparison.Ordinal); } private static string ReadRepoFile(string relativePath) { return File.ReadAllText(Path.Combine(RepoRoot(), relativePath.Replace('/', Path.DirectorySeparatorChar))); } private static IEnumerable EnumerateOmikaApiSourceFiles() { string apiRoot = Path.Combine(RepoRoot(), "src", "Omika.Api"); return Directory.EnumerateFiles(apiRoot, "*.cs", SearchOption.AllDirectories) .Where(f => !f.Contains(Path.DirectorySeparatorChar + "obj" + Path.DirectorySeparatorChar) && !f.Contains(Path.DirectorySeparatorChar + "bin" + Path.DirectorySeparatorChar)); } private static string RepoRoot() { var dir = new DirectoryInfo(AppContext.BaseDirectory); while (dir is not null && !File.Exists(Path.Combine(dir.FullName, "Omika.slnx"))) { dir = dir.Parent; } Assert.NotNull(dir); return dir!.FullName; } }