using System.Net; using w4c_workflows.Services.Security; using Xunit; namespace w4c_workflows.Tests; /// /// Pure policy tests: no DNS, no sockets. Every decision is driven by a /// fabricated address set so the reserved-range table is exercised exhaustively. /// public class EgressPolicyTests { private static PolicyDecision Evaluate( string url, params string[] addresses) => EgressTestData.Policy().Evaluate(new Uri(url), EgressTestData.Ips(addresses)); [Theory] [InlineData("127.0.0.1")] // IPv4 loopback [InlineData("127.10.20.30")] // whole 127/8 [InlineData("10.0.0.5")] // private [InlineData("172.16.0.1")] // private lower bound [InlineData("172.31.255.255")] // private upper bound [InlineData("192.168.1.1")] // private [InlineData("169.254.169.254")] // link-local / cloud metadata [InlineData("100.64.0.1")] // CGNAT [InlineData("100.127.255.255")] // CGNAT upper bound [InlineData("0.0.0.0")] // unspecified [InlineData("198.18.0.1")] // benchmarking [InlineData("255.255.255.255")] // broadcast [InlineData("::1")] // IPv6 loopback [InlineData("fc00::1")] // IPv6 unique-local [InlineData("fd12:3456::1")] // IPv6 unique-local [InlineData("fe80::1")] // IPv6 link-local [InlineData("::ffff:127.0.0.1")] // IPv4-mapped loopback [InlineData("::ffff:10.0.0.1")] // IPv4-mapped private public void Blocks_reserved_addresses(string address) { var decision = Evaluate("https://example.com/x", address); Assert.False(decision.Allowed); Assert.Equal("blocked_address", decision.Code); Assert.Contains(address, decision.Reason); } [Theory] [InlineData("93.184.216.34")] [InlineData("8.8.8.8")] [InlineData("172.32.0.1")] // just outside 172.16/12 [InlineData("100.128.0.1")] // just outside 100.64/10 [InlineData("2001:4860:4860::8888")] public void Allows_routable_addresses(string address) { Assert.True(Evaluate("https://example.com/x", address).Allowed); } [Fact] public void Blocks_when_any_resolved_address_is_reserved() { var decision = Evaluate("https://example.com/x", "93.184.216.34", "10.0.0.1"); Assert.False(decision.Allowed); Assert.Equal("blocked_address", decision.Code); } [Fact] public void Empty_address_set_fails_closed() { var decision = EgressTestData.Policy().CheckAddresses("gone.example.com", Array.Empty()); Assert.False(decision.Allowed); Assert.Equal("dns_failure", decision.Code); } [Fact] public void Allow_private_networks_option_permits_reserved_ranges() { var policy = EgressTestData.Policy(o => o.AllowPrivateNetworks = true); Assert.True(policy.Evaluate(new Uri("http://10.0.0.5/x"), EgressTestData.Ips("10.0.0.5")).Allowed); } [Theory] [InlineData("ftp")] [InlineData("file")] [InlineData("gopher")] public void Blocks_non_http_schemes(string scheme) { var url = scheme == "file" ? "file:///etc/passwd" : $"{scheme}://example.com/x"; var decision = EgressTestData.Policy().CheckHost(new Uri(url)); Assert.NotNull(decision); Assert.False(decision!.Allowed); Assert.Equal("scheme_not_allowed", decision.Code); } [Fact] public void Scheme_list_is_configurable() { var policy = EgressTestData.Policy(o => o.AllowedSchemes = ["https"]); var decision = policy.CheckHost(new Uri("http://example.com/x")); Assert.NotNull(decision); Assert.Equal("scheme_not_allowed", decision!.Code); } [Fact] public void Blocks_explicitly_denied_host() { var policy = EgressTestData.Policy(o => o.BlockedHosts = ["evil.example.com"]); var decision = policy.CheckHost(new Uri("https://evil.example.com/x")); Assert.NotNull(decision); Assert.False(decision!.Allowed); Assert.Equal("host_blocked", decision.Code); } [Fact] public void Deny_list_wins_over_allow_list() { var policy = EgressTestData.Policy(o => { o.AllowedHosts = ["*.example.com"]; o.BlockedHosts = ["admin.example.com"]; }); var decision = policy.CheckHost(new Uri("https://admin.example.com/x")); Assert.NotNull(decision); Assert.False(decision!.Allowed); Assert.Equal("host_blocked", decision.Code); } [Theory] [InlineData("https://internal.example.com/x", "internal.example.com")] [InlineData("https://api.example.com/x", "*.example.com")] [InlineData("https://api.example.com/x", ".example.com")] [InlineData("https://anything.example.com/x", "*")] [InlineData("https://EXAMPLE.com/x", "example.com")] // case-insensitive public void Allowed_host_bypasses_address_checks(string url, string pattern) { var policy = EgressTestData.Policy(o => o.AllowedHosts = [pattern]); // A private address would normally be rejected; the explicit allow-list // entry is the operator's opt-in and short-circuits that check. var decision = policy.Evaluate(new Uri(url), EgressTestData.Ips("10.0.0.5")); Assert.True(decision.Allowed); } [Fact] public void Wildcard_does_not_match_the_bare_suffix_host() { var policy = EgressTestData.Policy(o => o.AllowedHosts = ["*.example.com"]); // "*.example.com" matches subdomains only, mirroring cert wildcard rules. Assert.Null(policy.CheckHost(new Uri("https://example.com/x"))); } [Theory] [InlineData("sub.example.com", "*.example.com", true)] [InlineData("example.com", "*.example.com", false)] [InlineData("example.com", "example.com", true)] [InlineData("example.com.evil.com", "example.com", false)] public void Host_matcher_handles_wildcards_and_exact( string host, string pattern, bool expected) { Assert.Equal(expected, EgressPolicy.MatchesAny(host, new[] { pattern })); } [Fact] public void Max_redirects_never_goes_negative() { var policy = EgressTestData.Policy(o => o.MaxRedirects = -3); Assert.Equal(0, policy.MaxRedirects); } }