// ============================================================================= // SecurityAssertions.cs // Security-specific assertion helpers for testing // ============================================================================= using FluentAssertions; using System.Net; using System.Text.RegularExpressions; namespace StellaOps.Security.Tests.Infrastructure; /// /// Security-specific assertion methods for common security test patterns. /// public static partial class SecurityAssertions { /// /// Assert that a URL is safe (not an internal/metadata endpoint). /// public static void AssertUrlIsSafe(string url) { var uri = new Uri(url, UriKind.RelativeOrAbsolute); if (!uri.IsAbsoluteUri) return; // Check for localhost/loopback uri.Host.Should().NotBe("localhost", "URL should not point to localhost"); uri.Host.Should().NotBe("127.0.0.1", "URL should not point to loopback"); uri.Host.Should().NotBe("::1", "URL should not point to IPv6 loopback"); uri.Host.Should().NotBe("0.0.0.0", "URL should not point to all interfaces"); // Check for metadata endpoints uri.Host.Should().NotBe("169.254.169.254", "URL should not point to cloud metadata"); uri.Host.Should().NotContain("metadata.google.internal", "URL should not point to GCP metadata"); // Check for private IP ranges if (IPAddress.TryParse(uri.Host, out var ip)) { IsPrivateIp(ip).Should().BeFalse("URL should not point to private IP addresses"); } // Check for file:// scheme uri.Scheme.Should().NotBe("file", "URL should not use file:// scheme"); } /// /// Assert that a path does not contain traversal sequences. /// public static void AssertNoPathTraversal(string path) { path.Should().NotContain("..", "Path should not contain traversal sequences"); path.Should().NotContain("%2e%2e", "Path should not contain encoded traversal"); path.Should().NotContain("%252e", "Path should not contain double-encoded traversal"); path.Should().NotContain("\0", "Path should not contain null bytes"); } /// /// Assert that content is properly escaped for HTML context. /// public static void AssertHtmlEscaped(string content, string originalInput) { if (originalInput.Contains('<')) { content.Should().NotContain(" /// Assert that a command string is safe from injection. /// public static void AssertCommandSafe(string command) { var dangerousChars = new[] { ";", "|", "&", "`", "$(" }; foreach (var c in dangerousChars) { command.Should().NotContain(c, $"Command should not contain dangerous character: {c}"); } } /// /// Assert that an HTTP response indicates proper authorization failure. /// public static void AssertProperAuthorizationDenial(HttpStatusCode statusCode) { statusCode.Should().BeOneOf( HttpStatusCode.Unauthorized, HttpStatusCode.Forbidden, "Response should properly deny unauthorized access"); } /// /// Assert that no SQL injection was successful (result should not contain injected data). /// public static void AssertNoSqlInjectionSuccess(string response) { // Check for common signs that injection succeeded response.Should().NotMatchRegex(SqlPatternSuccess(), "Response should not indicate successful SQL injection"); } /// /// Assert that cryptographic parameters meet minimum strength requirements. /// public static void AssertCryptographicStrength(int keyBits, string algorithm) { algorithm.ToUpperInvariant().Should().NotBe("MD5", "MD5 should not be used for security"); algorithm.ToUpperInvariant().Should().NotBe("SHA1", "SHA1 should not be used for security"); if (algorithm.Contains("RSA", StringComparison.OrdinalIgnoreCase)) { keyBits.Should().BeGreaterOrEqualTo(2048, "RSA keys should be at least 2048 bits"); } else if (algorithm.Contains("AES", StringComparison.OrdinalIgnoreCase)) { keyBits.Should().BeGreaterOrEqualTo(128, "AES keys should be at least 128 bits"); } } /// /// Assert that a JWT token has proper structure and is not tampered with. /// public static void AssertJwtNotTampered(string token) { var parts = token.Split('.'); parts.Length.Should().Be(3, "JWT should have three parts"); parts[2].Should().NotBeEmpty("JWT signature should not be empty"); } /// /// Assert that headers do not contain injected values. /// public static void AssertNoHeaderInjection(IDictionary headers) { foreach (var header in headers) { header.Key.Should().NotContain("\r", "Header name should not contain CR"); header.Key.Should().NotContain("\n", "Header name should not contain LF"); header.Value.Should().NotContain("\r\n", "Header value should not contain CRLF"); } } /// /// Check if an IP address is in a private range. /// private static bool IsPrivateIp(IPAddress ip) { var bytes = ip.GetAddressBytes(); // IPv4 private ranges if (bytes.Length == 4) { // 10.0.0.0/8 if (bytes[0] == 10) return true; // 172.16.0.0/12 if (bytes[0] == 172 && bytes[1] >= 16 && bytes[1] <= 31) return true; // 192.168.0.0/16 if (bytes[0] == 192 && bytes[1] == 168) return true; // 127.0.0.0/8 (loopback) if (bytes[0] == 127) return true; // 169.254.0.0/16 (link-local) if (bytes[0] == 169 && bytes[1] == 254) return true; } return false; } [GeneratedRegex(@"(syntax error|mysql|postgresql|sqlite|ora-\d{5}|sql server)", RegexOptions.IgnoreCase)] private static partial Regex SqlPatternSuccess(); }