Files
Omika/tests/Omika.Tests/Api/ReverseProxySecurityTests.cs
T
2026-10-23 18:46:50 +02:00

119 lines
4.8 KiB
C#

namespace Omika.Tests.Api;
/// <summary>
/// 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.
/// </summary>
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<string> 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;
}
}