< Summary - Estud

Line coverage
50%
Covered lines: 26
Uncovered lines: 25
Coverable lines: 51
Total lines: 189
Line coverage: 50.9%
Branch coverage
34%
Covered branches: 22
Total branches: 64
Branch coverage: 34.3%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
File 1: SsoDomainRegex()100%11100%
File 2: ValidateSsoAuthority(...)83.33%6685.71%
File 2: ValidateSsoHost(...)50%101083.33%
File 2: NormalizeSsoDomain(...)50%7671.42%
File 2: ValidateSsoIpAddress(...)26.47%2953439.13%
File 2: IsPrivateSsoIpV4(...)0%7280%

File(s)

/_/Back/obj/Release/net10.0/System.Text.RegularExpressions.Generator/System.Text.RegularExpressions.Generator.RegexGenerator/RegexGenerator.g.cs

File '/_/Back/obj/Release/net10.0/System.Text.RegularExpressions.Generator/System.Text.RegularExpressions.Generator.RegexGenerator/RegexGenerator.g.cs' does not exist (any more).

/home/runner/work/syki/syki/Back/Extensions/SsoExtensions.cs

#LineLine coverage
 1using System.Net;
 2using System.Net.Sockets;
 3using System.Text.RegularExpressions;
 4
 5namespace Estud.Back.Extensions;
 6
 7public static partial class SsoExtensions
 8{
 9    extension(string value)
 10    {
 11        public EstudError? ValidateSsoAuthority()
 12        {
 13            // Must be valid URI
 2814            if (!Uri.TryCreate(value, UriKind.Absolute, out var uri))
 015                return InvalidSsoAuthority.I;
 16
 17            // Must be HTTPS
 2818            if (uri.Scheme != Uri.UriSchemeHttps)
 419                return SsoAuthorityMustBeHttps.I;
 20
 21            // Block URLs with userinfo (SSRF bypass: https://evil.com@169.254.169.254/)
 2422            if (uri.UserInfo.HasValue())
 423                return SsoAuthorityHasUserInfo.I;
 24
 25            // Parse and validate the host
 2026            return uri.Host.ValidateSsoHost();
 27        }
 28
 29        public EstudError? ValidateSsoHost()
 30        {
 31            // Try to parse as IP address
 2032            if (IPAddress.TryParse(value, out var ip))
 433                return ip.ValidateSsoIpAddress();
 34
 35            // It's a hostname - check for dangerous hostnames
 1636            var lowerHost = value.ToLowerInvariant();
 37
 38            // Block localhost variants (allow in dev/testing)
 1639            if (lowerHost is "localhost" or "localhost.localdomain")
 040                return EnvironmentExtensions.IsDevelopmentOrTesting() ? null : SsoAuthorityLocalhostNotAllowed.I;
 41
 1642            return null;
 43        }
 44
 45        public string? NormalizeSsoDomain()
 46        {
 1247            if (value.IsEmpty()) return null;
 48
 1249            var normalized = value.Trim().ToLowerInvariant();
 50
 51            // Remove @ if present at start
 1252            if (normalized.StartsWith('@'))
 053                normalized = normalized[1..];
 54
 55            // Basic domain validation
 1256            if (!SsoDomainRegex().IsMatch(normalized))
 057                return null;
 58
 1259            return normalized;
 60        }
 61    }
 62
 63    extension(IPAddress ip)
 64    {
 65        public EstudError? ValidateSsoIpAddress()
 66        {
 467            var resolved = ip;
 468            var isDevOrTest = EnvironmentExtensions.IsDevelopmentOrTesting();
 69
 70            // Handle IPv4-mapped IPv6 addresses (::ffff:127.0.0.1)
 471            if (resolved.IsIPv4MappedToIPv6)
 072                resolved = resolved.MapToIPv4();
 73
 74            // IPv6 checks
 475            if (resolved.AddressFamily == AddressFamily.InterNetworkV6)
 76            {
 77                // Block IPv6 loopback (::1) — allow in dev/testing
 078                if (IPAddress.IPv6Loopback.Equals(resolved))
 079                    return isDevOrTest ? null : SsoAuthorityLoopbackNotAllowed.I;
 80
 81                // Block IPv6 link-local (fe80::/10) — always blocked (cloud metadata risk)
 082                if (resolved.IsIPv6LinkLocal)
 083                    return SsoAuthorityLinkLocalNotAllowed.I;
 84
 85                // Block IPv6 unique local addresses (fc00::/7 = fc00:: and fd00::) — allow in dev/testing
 086                var bytes = resolved.GetAddressBytes();
 087                if ((bytes[0] & 0xFE) == 0xFC) // fc00::/7
 088                    return isDevOrTest ? null : SsoAuthorityPrivateIpNotAllowed.I;
 89
 090                return null;
 91            }
 92
 93            // IPv4 checks
 494            var ipBytes = resolved.GetAddressBytes();
 95
 96            // Block 0.0.0.0 — always blocked
 497            if (ipBytes[0] == 0 && ipBytes[1] == 0 && ipBytes[2] == 0 && ipBytes[3] == 0)
 098                return SsoAuthorityLoopbackNotAllowed.I;
 99
 100            // Block entire loopback range 127.0.0.0/8 — allow in dev/testing
 4101            if (ipBytes[0] == 127)
 0102                return isDevOrTest ? null : SsoAuthorityLoopbackNotAllowed.I;
 103
 104            // Block entire link-local range 169.254.0.0/16 — always blocked (cloud metadata risk)
 4105            if (ipBytes[0] == 169 && ipBytes[1] == 254)
 4106                return SsoAuthorityLinkLocalNotAllowed.I;
 107
 108            // Block private IP ranges — allow in dev/testing
 0109            if (IsPrivateSsoIpV4(ipBytes))
 0110                return isDevOrTest ? null : SsoAuthorityPrivateIpNotAllowed.I;
 111
 0112            return null;
 113        }
 114    }
 115
 116    private static bool IsPrivateSsoIpV4(byte[] bytes)
 117    {
 0118        return bytes[0] switch
 0119        {
 0120            10 => true,                                      // 10.0.0.0/8
 0121            172 => bytes[1] >= 16 && bytes[1] <= 31,         // 172.16.0.0/12
 0122            192 => bytes[1] == 168,                          // 192.168.0.0/16
 0123            _ => false
 0124        };
 125    }
 126
 127    [GeneratedRegex(@"^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$")]
 128    private static partial Regex SsoDomainRegex();
 129}