├── .github └── workflows │ └── build-and-test.yml ├── .gitignore ├── Examples ├── firewall1.txt └── firewall2.txt ├── FirewallAnalysis.Tests ├── AddressRangeTest.cs ├── FirewallAnalysis.Tests.csproj ├── NetworkProtocolTest.cs ├── PortRangeTest.cs ├── WindowsFirewallEquivalenceCheckTest.cs ├── WindowsFirewallRuleParserTest.cs ├── WindowsFirewallRuleTest.cs └── WindowsFirewallTest.cs ├── FirewallAnalysis ├── AddressRange.cs ├── AddressSet.cs ├── AssemblyInfo.cs ├── FirewallAnalysis.csproj ├── NetworkProtocol.cs ├── PortRange.cs ├── PortSet.cs ├── RetrieveModelValue.cs ├── WindowsFirewall.cs ├── WindowsFirewallEquivalenceCheck.cs ├── WindowsFirewallInconsistency.cs ├── WindowsFirewallPacket.cs ├── WindowsFirewallPacketVariables.cs ├── WindowsFirewallRule.cs └── WindowsFirewallRuleParser.cs ├── FirewallChecker.sln ├── FirewallEquivalenceCheckerCmd ├── FirewallEquivalenceCheckerCmd.csproj ├── Options.cs └── Program.cs ├── FirewallQueryCmd ├── FirewallQueryCmd.csproj ├── Options.cs └── Program.cs ├── LICENSE └── README.md /.github/workflows/build-and-test.yml: -------------------------------------------------------------------------------- 1 | name: build-and-test 2 | on: 3 | push: 4 | branches: master 5 | pull_request: 6 | branches: master 7 | jobs: 8 | build-and-test-projects: 9 | runs-on: ${{ matrix.os }} 10 | strategy: 11 | matrix: 12 | os: [windows-latest, ubuntu-latest, macos-latest] 13 | fail-fast: false 14 | steps: 15 | - uses: actions/checkout@v2 16 | - uses: actions/setup-dotnet@v1 17 | with: 18 | dotnet-version: '5.0.x' 19 | - run: dotnet build 20 | - run: dotnet test 21 | - run: dotnet run --project FirewallEquivalenceCheckerCmd --firewall1 Examples/firewall1.txt --firewall2 Examples/firewall2.txt 22 | - run: dotnet run --project FirewallQueryCmd --firewall Examples/firewall1.txt --srcAddress 10.3.141.0 --srcPort 100 --dstPort 100 --protocol UDP 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vs 2 | .vscode 3 | obj 4 | */obj 5 | */bin 6 | packages 7 | .gitattributes 8 | -------------------------------------------------------------------------------- /Examples/firewall1.txt: -------------------------------------------------------------------------------- 1 | Name Enabled Action Local Port Remote Address Remote Port Protocol 2 | Foo1 Yes Allow 100 10.3.141.0 100 UDP 3 | Bar1 Yes Allow 200 10.3.141.0 200 TCP -------------------------------------------------------------------------------- /Examples/firewall2.txt: -------------------------------------------------------------------------------- 1 | Name Enabled Action Local Port Remote Address Remote Port Protocol 2 | Foo2 Yes Allow 100 10.3.141.0 100 UDP 3 | Bar2 Yes Allow 200 10.3.141.1 200 TCP -------------------------------------------------------------------------------- /FirewallAnalysis.Tests/AddressRangeTest.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.FirewallAnalysis.Tests 6 | { 7 | using System.Net; 8 | using Microsoft.FirewallAnalysis; 9 | using Microsoft.VisualStudio.TestTools.UnitTesting; 10 | using Microsoft.Z3; 11 | 12 | /// 13 | /// Unit tests for the class. 14 | /// 15 | [TestClass] 16 | public class AddressRangeTest 17 | { 18 | /// 19 | /// Test Z3 finds an address value in a satisfiable range. 20 | /// 21 | [TestMethod] 22 | public void TestSatisfiableRange() 23 | { 24 | using (var ctx = new Context()) 25 | { 26 | IPAddress low = IPAddress.Parse("127.0.0.1"); 27 | IPAddress high = IPAddress.Parse("127.0.0.10"); 28 | var range = new AddressRange 29 | { 30 | Low = low, 31 | High = high 32 | }; 33 | 34 | string variableName = "address"; 35 | BitVecExpr variable = ctx.MkConst(variableName, ctx.MkBitVecSort(32)) as BitVecExpr; 36 | Assert.IsNotNull(variable); 37 | Solver s = ctx.MkSolver(); 38 | s.Assert(range.Contains(ctx, variable)); 39 | Status result = s.Check(); 40 | Assert.AreEqual(Status.SATISFIABLE, result); 41 | IPAddress binding; 42 | Assert.IsTrue(RetrieveModelValue.TryRetrieveAddress(variableName, s.Model, out binding)); 43 | Assert.IsTrue(AddressRangeTest.CompareAddresses(low, binding) >= 0); 44 | Assert.IsTrue(AddressRangeTest.CompareAddresses(high, binding) <= 0); 45 | } 46 | } 47 | 48 | /// 49 | /// Test Z3 binds to the single possible address value. 50 | /// 51 | [TestMethod] 52 | public void TestSingle() 53 | { 54 | using (var ctx = new Context()) 55 | { 56 | IPAddress single = IPAddress.Parse("192.168.0.1"); 57 | var range = new AddressRange 58 | { 59 | Low = single, 60 | High = single 61 | }; 62 | 63 | string variableName = "address"; 64 | BitVecExpr variable = ctx.MkConst(variableName, ctx.MkBitVecSort(32)) as BitVecExpr; 65 | Solver s = ctx.MkSolver(); 66 | s.Assert(range.Contains(ctx, variable)); 67 | Status result = s.Check(); 68 | Assert.AreEqual(Status.SATISFIABLE, result); 69 | IPAddress binding; 70 | Assert.IsTrue(RetrieveModelValue.TryRetrieveAddress(variableName, s.Model, out binding)); 71 | Assert.AreEqual(single, binding); 72 | } 73 | } 74 | 75 | /// 76 | /// Tests Z3 is unable to find an address in an invalid address range. 77 | /// 78 | [TestMethod] 79 | public void TestUnsatisfiableRange() 80 | { 81 | using (var ctx = new Context()) 82 | { 83 | IPAddress low = IPAddress.Parse("192.168.0.1"); 84 | IPAddress high = IPAddress.Parse("192.0.100.0"); 85 | var range = new AddressRange 86 | { 87 | Low = low, 88 | High = high 89 | }; 90 | 91 | string variableName = "address"; 92 | BitVecExpr variable = ctx.MkConst(variableName, ctx.MkBitVecSort(32)) as BitVecExpr; 93 | Solver s = ctx.MkSolver(); 94 | s.Assert(range.Contains(ctx, variable)); 95 | Status result = s.Check(); 96 | Assert.AreEqual(Status.UNSATISFIABLE, result); 97 | } 98 | } 99 | 100 | /// 101 | /// Tests Z3 is unable to find an address outside the full range of addresses. 102 | /// 103 | [TestMethod] 104 | public void TestFullRange() 105 | { 106 | using (var ctx = new Context()) 107 | { 108 | IPAddress low = IPAddress.Parse("0.0.0.0"); 109 | IPAddress high = IPAddress.Parse("255.255.255.255"); 110 | var range = new AddressRange 111 | { 112 | Low = low, 113 | High = high 114 | }; 115 | 116 | string variableName = "address"; 117 | BitVecExpr variable = ctx.MkConst(variableName, ctx.MkBitVecSort(32)) as BitVecExpr; 118 | Solver s = ctx.MkSolver(); 119 | s.Assert(ctx.MkNot(range.Contains(ctx, variable))); 120 | Status result = s.Check(); 121 | Assert.AreEqual(Status.UNSATISFIABLE, result); 122 | } 123 | } 124 | 125 | /// 126 | /// Compares two IP addresses. Returns zero if equal, positive number if 127 | /// second greater than first, and negative if first greater than second. 128 | /// 129 | /// First IP address to compare. 130 | /// Second IP address to compare. 131 | /// A comparison between the given addresses. 132 | private static int CompareAddresses(IPAddress first, IPAddress second) 133 | { 134 | byte[] a = first.GetAddressBytes(); 135 | byte[] b = second.GetAddressBytes(); 136 | for (int i = 0; i < 4; i++) 137 | { 138 | if (a[i] != b[i]) 139 | { 140 | return b[i] - a[i]; 141 | } 142 | } 143 | 144 | return 0; 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /FirewallAnalysis.Tests/FirewallAnalysis.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Microsoft.FirewallAnalysis.Tests 5 | netcoreapp5.0 6 | x64 7 | false 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /FirewallAnalysis.Tests/NetworkProtocolTest.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.FirewallAnalysis.Tests 6 | { 7 | using Microsoft.FirewallAnalysis; 8 | using Microsoft.VisualStudio.TestTools.UnitTesting; 9 | using Microsoft.Z3; 10 | 11 | /// 12 | /// Unit tests for the class. 13 | /// 14 | [TestClass] 15 | public class NetworkProtocolTest 16 | { 17 | /// 18 | /// Test case where is true. 19 | /// 20 | [TestMethod] 21 | public void TestMatchAny() 22 | { 23 | using (var ctx = new Context()) 24 | { 25 | var networkProtocol = new NetworkProtocol 26 | { 27 | Any = true 28 | }; 29 | 30 | string protocolVariableName = "protocol"; 31 | BitVecExpr protocolVariable = ctx.MkConst(protocolVariableName, ctx.MkBitVecSort(8)) as BitVecExpr; 32 | Solver s = ctx.MkSolver(); 33 | s.Assert(ctx.MkNot(networkProtocol.Matches(ctx, protocolVariable))); 34 | Status result = s.Check(); 35 | Assert.AreEqual(Status.UNSATISFIABLE, result); 36 | } 37 | } 38 | 39 | /// 40 | /// Tests Z3 correctly binds a variable to the protocol number. 41 | /// 42 | [TestMethod] 43 | public void TestMatchesSatisfiable() 44 | { 45 | using (var ctx = new Context()) 46 | { 47 | int protocolNumber = 6; 48 | var networkProtocol = new NetworkProtocol 49 | { 50 | Any = false, 51 | ProtocolNumber = protocolNumber 52 | }; 53 | 54 | string protocolVariableName = "protocol"; 55 | BitVecExpr protocolVariable = ctx.MkConst(protocolVariableName, ctx.MkBitVecSort(8)) as BitVecExpr; 56 | Solver s = ctx.MkSolver(); 57 | s.Assert(networkProtocol.Matches(ctx, protocolVariable)); 58 | Status result = s.Check(); 59 | Assert.AreEqual(Status.SATISFIABLE, result); 60 | int binding; 61 | Assert.IsTrue(RetrieveModelValue.TryRetrieveInteger(protocolVariableName, s.Model, out binding)); 62 | Assert.AreEqual(protocolNumber, binding); 63 | } 64 | } 65 | 66 | /// 67 | /// Tests Z3 correctly avoids binding variable to protocol number. 68 | /// 69 | [TestMethod] 70 | public void TestMatchesUnsatisfiable() 71 | { 72 | using (var ctx = new Context()) 73 | { 74 | int protocolNumber = 6; 75 | var networkProtocol = new NetworkProtocol 76 | { 77 | Any = false, 78 | ProtocolNumber = protocolNumber 79 | }; 80 | 81 | string protocolVariableName = "protocol"; 82 | BitVecExpr protocolVariable = ctx.MkConst(protocolVariableName, ctx.MkBitVecSort(8)) as BitVecExpr; 83 | Solver s = ctx.MkSolver(); 84 | s.Assert(ctx.MkNot(networkProtocol.Matches(ctx, protocolVariable))); 85 | Status result = s.Check(); 86 | Assert.AreEqual(Status.SATISFIABLE, result); 87 | int binding; 88 | Assert.IsTrue(RetrieveModelValue.TryRetrieveInteger(protocolVariableName, s.Model, out binding)); 89 | Assert.AreNotEqual(protocolNumber, binding); 90 | } 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /FirewallAnalysis.Tests/PortRangeTest.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.FirewallAnalysis.Tests 6 | { 7 | using Microsoft.FirewallAnalysis; 8 | using Microsoft.VisualStudio.TestTools.UnitTesting; 9 | using Microsoft.Z3; 10 | 11 | /// 12 | /// Unit tests for the class. 13 | /// 14 | [TestClass] 15 | public class PortRangeTest 16 | { 17 | /// 18 | /// Test Z3 finds a port value in a satisfiable range. 19 | /// 20 | [TestMethod] 21 | public void TestSatisfiableRange() 22 | { 23 | using (var ctx = new Context()) 24 | { 25 | int low = 8; 26 | int high = 16; 27 | var range = new PortRange 28 | { 29 | Low = low, 30 | High = high 31 | }; 32 | 33 | string portVariableName = "port"; 34 | BitVecExpr variable = ctx.MkConst(portVariableName, ctx.MkBitVecSort(16)) as BitVecExpr; 35 | Solver s = ctx.MkSolver(); 36 | s.Assert(range.Contains(ctx, variable)); 37 | Status result = s.Check(); 38 | Assert.AreEqual(Status.SATISFIABLE, result); 39 | int binding; 40 | Assert.IsTrue(RetrieveModelValue.TryRetrieveInteger(portVariableName, s.Model, out binding)); 41 | Assert.IsTrue(binding >= low); 42 | Assert.IsTrue(binding <= high); 43 | } 44 | } 45 | 46 | /// 47 | /// Tests Z3 finds a port value in a range containing only one port. 48 | /// 49 | [TestMethod] 50 | public void TestSingle() 51 | { 52 | using (var ctx = new Context()) 53 | { 54 | int single = 64; 55 | var range = new PortRange 56 | { 57 | Low = single, 58 | High = single 59 | }; 60 | 61 | string portVariableName = "port"; 62 | BitVecExpr variable = ctx.MkConst(portVariableName, ctx.MkBitVecSort(16)) as BitVecExpr; 63 | Solver s = ctx.MkSolver(); 64 | s.Assert(range.Contains(ctx, variable)); 65 | Status result = s.Check(); 66 | Assert.AreEqual(Status.SATISFIABLE, result); 67 | int binding; 68 | Assert.IsTrue(RetrieveModelValue.TryRetrieveInteger(portVariableName, s.Model, out binding)); 69 | Assert.AreEqual(single, binding); 70 | } 71 | } 72 | 73 | /// 74 | /// Tests Z3 is unable to find a port value in an unsatisfiable port range. 75 | /// 76 | [TestMethod] 77 | public void TestUnsatisfiableRange() 78 | { 79 | using (var ctx = new Context()) 80 | { 81 | var range = new PortRange 82 | { 83 | Low = 16, 84 | High = 8 85 | }; 86 | 87 | string portVariableName = "port"; 88 | BitVecExpr variable = ctx.MkConst(portVariableName, ctx.MkBitVecSort(16)) as BitVecExpr; 89 | Solver s = ctx.MkSolver(); 90 | s.Assert(range.Contains(ctx, variable)); 91 | Status result = s.Check(); 92 | Assert.AreEqual(Status.UNSATISFIABLE, result); 93 | } 94 | } 95 | 96 | /// 97 | /// Tests Z3 is unable to find a port value outside the full 16-bit port range. 98 | /// 99 | [TestMethod] 100 | public void TestFullRange() 101 | { 102 | using (var ctx = new Context()) 103 | { 104 | var range = new PortRange 105 | { 106 | Low = 0, 107 | High = ushort.MaxValue 108 | }; 109 | 110 | string portVariableName = "port"; 111 | BitVecExpr variable = ctx.MkConst(portVariableName, ctx.MkBitVecSort(16)) as BitVecExpr; 112 | Solver s = ctx.MkSolver(); 113 | s.Assert(ctx.MkNot(range.Contains(ctx, variable))); 114 | Status result = s.Check(); 115 | Assert.AreEqual(Status.UNSATISFIABLE, result); 116 | } 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /FirewallAnalysis.Tests/WindowsFirewallEquivalenceCheckTest.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.FirewallAnalysis.Tests 6 | { 7 | using System.Collections.Generic; 8 | using System.Linq; 9 | using System.Net; 10 | using Microsoft.FirewallAnalysis; 11 | using Microsoft.VisualStudio.TestTools.UnitTesting; 12 | 13 | /// 14 | /// Unit tests for the class. 15 | /// 16 | [TestClass] 17 | public class WindowsFirewallEquivalenceCheckTest 18 | { 19 | /// 20 | /// Tests that a single firewall is equivalent with itself. 21 | /// 22 | [TestMethod] 23 | public void TestSameFirewall() 24 | { 25 | string record = "X\tYes\tAllow\t80\t192.168.1.1\t128\t6"; 26 | string record2 = "Y\tYes\tAllow\t8080\t127.0.0.1\t256\t6"; 27 | string text = $"{WindowsFirewallRuleParserTest.HeaderText}\n{record}\n{record2}"; 28 | var firewall = new WindowsFirewall 29 | { 30 | BlockByDefault = true, 31 | Rules = WindowsFirewallRuleParser.Parse(text, '\t').ToList() 32 | }; 33 | 34 | var inconsistencies = WindowsFirewallEquivalenceCheck.CheckEquivalence(firewall, firewall); 35 | Assert.IsFalse(inconsistencies.Any()); 36 | } 37 | 38 | /// 39 | /// Tests that two firewalls with the same rules are equivalent. 40 | /// 41 | [TestMethod] 42 | public void TestEqualFirewalls() 43 | { 44 | string record = "X\tYes\tAllow\t80\t192.168.1.1\t128\t6"; 45 | string record2 = "Y\tYes\tAllow\t8080\t127.0.0.1\t256\t6"; 46 | string text = $"{WindowsFirewallRuleParserTest.HeaderText}\n{record}\n{record2}"; 47 | var f1 = new WindowsFirewall 48 | { 49 | BlockByDefault = true, 50 | Rules = WindowsFirewallRuleParser.Parse(text, '\t').ToList() 51 | }; 52 | 53 | var f2 = new WindowsFirewall 54 | { 55 | BlockByDefault = true, 56 | Rules = WindowsFirewallRuleParser.Parse(text, '\t').ToList() 57 | }; 58 | 59 | var inconstistencies = WindowsFirewallEquivalenceCheck.CheckEquivalence(f1, f2); 60 | Assert.IsFalse(inconstistencies.Any()); 61 | } 62 | 63 | /// 64 | /// Tests that two firewalls with different rules but equal action are equivalent. 65 | /// 66 | [TestMethod] 67 | public void TestEquivalentFirewalls() 68 | { 69 | string record = "X\tYes\tAllow\t80\t192.168.1.0-192.168.1.10\t128\t6"; 70 | string text = $"{WindowsFirewallRuleParserTest.HeaderText}\n{record}"; 71 | var f1 = new WindowsFirewall 72 | { 73 | BlockByDefault = true, 74 | Rules = WindowsFirewallRuleParser.Parse(text, '\t').ToList() 75 | }; 76 | 77 | record = "X\tYes\tAllow\t80\t192.168.1.0-192.168.1.5\t128\t6"; 78 | string record2 = "Y\tYes\tAllow\t80\t192.168.1.6-192.168.1.10\t128\t6"; 79 | text = $"{WindowsFirewallRuleParserTest.HeaderText}\n{record}\n{record2}"; 80 | var f2 = new WindowsFirewall 81 | { 82 | BlockByDefault = true, 83 | Rules = WindowsFirewallRuleParser.Parse(text, '\t').ToList() 84 | }; 85 | 86 | var inconstistencies = WindowsFirewallEquivalenceCheck.CheckEquivalence(f1, f2); 87 | Assert.IsFalse(inconstistencies.Any()); 88 | } 89 | 90 | /// 91 | /// Tests equivalence check of firewalls with a single packet difference. 92 | /// 93 | [TestMethod] 94 | public void TestSingleDifference() 95 | { 96 | int localPort = 80; 97 | int remotePort = 128; 98 | int protocol = 6; 99 | string record = $"X\tYes\tAllow\t{localPort}\t192.168.1.0-192.168.1.10\t{remotePort}\t{protocol}"; 100 | string text = $"{WindowsFirewallRuleParserTest.HeaderText}\n{record}"; 101 | var f1 = new WindowsFirewall 102 | { 103 | Name = "1", 104 | BlockByDefault = true, 105 | Rules = WindowsFirewallRuleParser.Parse(text, '\t').ToList() 106 | }; 107 | 108 | record = $"X\tYes\tAllow\t{localPort}\t192.168.1.0-192.168.1.4\t{remotePort}\t{protocol}"; 109 | string record2 = $"Y\tYes\tAllow\t{localPort}\t192.168.1.6-192.168.1.10\t{remotePort}\t{protocol}"; 110 | text = $"{WindowsFirewallRuleParserTest.HeaderText}\n{record}\n{record2}"; 111 | var f2 = new WindowsFirewall 112 | { 113 | Name = "2", 114 | BlockByDefault = true, 115 | Rules = WindowsFirewallRuleParser.Parse(text, '\t').ToList() 116 | }; 117 | 118 | var inconstistencies = WindowsFirewallEquivalenceCheck.CheckEquivalence(f1, f2).ToList(); 119 | Assert.AreEqual(1, inconstistencies.Count); 120 | WindowsFirewallInconsistency inconsistency = inconstistencies.Single(); 121 | Assert.AreEqual("1", inconsistency.Firewalls.Item1.Name); 122 | Assert.IsTrue(inconsistency.Allowed.Item1); 123 | Assert.AreEqual("2", inconsistency.Firewalls.Item2.Name); 124 | Assert.IsFalse(inconsistency.Allowed.Item2); 125 | Assert.AreEqual(1, inconsistency.RuleMatches.Item1.Count); 126 | Assert.AreEqual("X", inconsistency.RuleMatches.Item1.Single().Name); 127 | Assert.AreEqual(0, inconsistency.RuleMatches.Item2.Count); 128 | Assert.AreEqual(IPAddress.Parse("192.168.1.5"), inconsistency.Packet.SourceAddress); 129 | Assert.AreEqual(remotePort, inconsistency.Packet.SourcePort); 130 | Assert.AreEqual(localPort, inconsistency.Packet.DestinationPort); 131 | Assert.AreEqual(protocol, inconsistency.Packet.Protocol); 132 | } 133 | 134 | /// 135 | /// Tests equivalence check of firewalls with multiple packet differences. 136 | /// 137 | [TestMethod] 138 | public void TestMultipleDifferences() 139 | { 140 | int localPort = 8080; 141 | int remotePort = 256; 142 | int protocol = 17; 143 | string record = $"X\tYes\tAllow\t{localPort}\t255.255.255.0-255.255.255.15\t{remotePort}\t{protocol}"; 144 | string text = $"{WindowsFirewallRuleParserTest.HeaderText}\n{record}"; 145 | var f1 = new WindowsFirewall 146 | { 147 | Name = "1", 148 | BlockByDefault = true, 149 | Rules = WindowsFirewallRuleParser.Parse(text, '\t').ToList() 150 | }; 151 | 152 | record = $"X\tYes\tAllow\t{localPort}\t255.255.255.0-255.255.255.2\t{remotePort}\t{protocol}"; 153 | string record2 = $"Y\tYes\tAllow\t{localPort}\t255.255.255.4-255.255.255.6\t{remotePort}\t{protocol}"; 154 | string record3 = $"Y\tYes\tAllow\t{localPort}\t255.255.255.8-255.255.255.10\t{remotePort}\t{protocol}"; 155 | string record4 = $"Y\tYes\tAllow\t{localPort}\t255.255.255.12-255.255.255.15\t{remotePort}\t{protocol}"; 156 | text = $"{WindowsFirewallRuleParserTest.HeaderText}\n{record}\n{record2}\n{record3}\n{record4}"; 157 | var f2 = new WindowsFirewall 158 | { 159 | Name = "2", 160 | BlockByDefault = true, 161 | Rules = WindowsFirewallRuleParser.Parse(text, '\t').ToList() 162 | }; 163 | 164 | var inconsistencies = WindowsFirewallEquivalenceCheck.CheckEquivalence(f1, f2).ToList(); 165 | var expected = new HashSet 166 | { 167 | IPAddress.Parse("255.255.255.3"), 168 | IPAddress.Parse("255.255.255.7"), 169 | IPAddress.Parse("255.255.255.11") 170 | }; 171 | 172 | Assert.AreEqual(3, inconsistencies.Count); 173 | foreach (var inconsistency in inconsistencies) 174 | { 175 | Assert.AreEqual("1", inconsistency.Firewalls.Item1.Name); 176 | Assert.IsTrue(inconsistency.Allowed.Item1); 177 | Assert.AreEqual("2", inconsistency.Firewalls.Item2.Name); 178 | Assert.IsFalse(inconsistency.Allowed.Item2); 179 | Assert.AreEqual(1, inconsistency.RuleMatches.Item1.Count); 180 | Assert.AreEqual("X", inconsistency.RuleMatches.Item1.Single().Name); 181 | Assert.AreEqual(0, inconsistency.RuleMatches.Item2.Count); 182 | Assert.AreEqual(remotePort, inconsistency.Packet.SourcePort); 183 | Assert.AreEqual(localPort, inconsistency.Packet.DestinationPort); 184 | Assert.AreEqual(protocol, inconsistency.Packet.Protocol); 185 | Assert.IsTrue(expected.Contains(inconsistency.Packet.SourceAddress)); 186 | expected.Remove(inconsistency.Packet.SourceAddress); 187 | } 188 | 189 | Assert.IsFalse(expected.Any()); 190 | } 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /FirewallAnalysis.Tests/WindowsFirewallRuleParserTest.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.FirewallAnalysis 6 | { 7 | using System.Collections.Generic; 8 | using System.Linq; 9 | using System.Net; 10 | using Microsoft.FirewallAnalysis; 11 | using Microsoft.VisualStudio.TestTools.UnitTesting; 12 | 13 | /// 14 | /// Unit tests for the class. 15 | /// 16 | [TestClass] 17 | public class WindowsFirewallRuleParserTest 18 | { 19 | /// 20 | /// Gets a map from column header to row index. 21 | /// 22 | internal static Dictionary HeaderIndex { get; } = new Dictionary 23 | { 24 | { WindowsFirewallRuleParser.RuleNameName, 0 }, 25 | { WindowsFirewallRuleParser.EnabledHeaderName, 1 }, 26 | { WindowsFirewallRuleParser.PermissionHeaderName, 2 }, 27 | { WindowsFirewallRuleParser.LocalPortHeaderName, 3 }, 28 | { WindowsFirewallRuleParser.RemoteAddressHeaderName, 4 }, 29 | { WindowsFirewallRuleParser.RemotePortHeaderName, 5 }, 30 | { WindowsFirewallRuleParser.ProtocolHeaderName, 6 } 31 | }; 32 | 33 | /// 34 | /// Gets unparsed header text corresponding to . 35 | /// 36 | internal static string HeaderText 37 | { 38 | get 39 | { 40 | var reverseIndexed = new string[WindowsFirewallRuleParserTest.HeaderIndex.Count]; 41 | foreach (KeyValuePair pair in WindowsFirewallRuleParserTest.HeaderIndex) 42 | { 43 | reverseIndexed[pair.Value] = pair.Key; 44 | } 45 | 46 | return string.Join("\t", reverseIndexed); 47 | } 48 | } 49 | 50 | /// 51 | /// Tests header is correctly parsed. 52 | /// 53 | [TestMethod] 54 | public void TestParseHeader() 55 | { 56 | Dictionary indexed = WindowsFirewallRuleParser.ParseHeader( 57 | WindowsFirewallRuleParser.RequiredHeaders, 58 | WindowsFirewallRuleParserTest.HeaderText, 59 | '\t'); 60 | 61 | foreach (KeyValuePair pair in WindowsFirewallRuleParserTest.HeaderIndex) 62 | { 63 | Assert.IsTrue(indexed.ContainsKey(pair.Key)); 64 | Assert.AreEqual(pair.Value, indexed[pair.Key]); 65 | } 66 | } 67 | 68 | /// 69 | /// Tests parsing of a simple rule with only single ports and addresses. 70 | /// 71 | [TestMethod] 72 | public void TestParseRuleSingle() 73 | { 74 | int localPort = 80; 75 | IPAddress remoteAddress = IPAddress.Parse("192.168.1.1"); 76 | int remotePort = 128; 77 | string record = $"X\tYes\tAllow\t{localPort}\t{remoteAddress}\t{remotePort}\tTCP"; 78 | WindowsFirewallRule rule = WindowsFirewallRuleParser.ParseRecord( 79 | WindowsFirewallRuleParserTest.HeaderIndex, 80 | record, 81 | '\t'); 82 | Assert.AreEqual("X", rule.Name); 83 | Assert.IsTrue(rule.Enabled); 84 | Assert.IsTrue(rule.Allow); 85 | Assert.AreEqual(1, rule.LocalPorts.Ranges.Count); 86 | Assert.AreEqual(localPort, rule.LocalPorts.Ranges.Single().Low); 87 | Assert.AreEqual(localPort, rule.LocalPorts.Ranges.Single().High); 88 | Assert.AreEqual(1, rule.RemotePorts.Ranges.Count); 89 | Assert.AreEqual(remotePort, rule.RemotePorts.Ranges.Single().Low); 90 | Assert.AreEqual(remotePort, rule.RemotePorts.Ranges.Single().High); 91 | Assert.AreEqual(1, rule.RemoteAddresses.Ranges.Count); 92 | Assert.AreEqual(remoteAddress, rule.RemoteAddresses.Ranges.Single().Low); 93 | Assert.AreEqual(remoteAddress, rule.RemoteAddresses.Ranges.Single().High); 94 | Assert.AreEqual(6, rule.Protocol.ProtocolNumber); 95 | } 96 | 97 | /// 98 | /// Tests parsing of a rule with ranges of ports and addresses. 99 | /// 100 | [TestMethod] 101 | public void TestParseRuleRanges() 102 | { 103 | int localPortLow = 80; 104 | int localPortHigh = 8080; 105 | IPAddress remoteAddressLow = IPAddress.Parse("64.32.16.8"); 106 | IPAddress remoteAddressHigh = IPAddress.Parse("128.64.32.16"); 107 | int remotePortLow = 128; 108 | int remotePortHigh = 256; 109 | string record = $"X\tYes\tAllow\t{localPortLow}-{localPortHigh}\t" + 110 | $"{remoteAddressLow}-{remoteAddressHigh}\t" + 111 | $"{remotePortLow}-{remotePortHigh}\tUDP"; 112 | WindowsFirewallRule rule = WindowsFirewallRuleParser.ParseRecord( 113 | WindowsFirewallRuleParserTest.HeaderIndex, 114 | record, 115 | '\t'); 116 | Assert.AreEqual(1, rule.LocalPorts.Ranges.Count); 117 | Assert.AreEqual(localPortLow, rule.LocalPorts.Ranges.Single().Low); 118 | Assert.AreEqual(localPortHigh, rule.LocalPorts.Ranges.Single().High); 119 | Assert.AreEqual(1, rule.RemotePorts.Ranges.Count); 120 | Assert.AreEqual(remotePortLow, rule.RemotePorts.Ranges.Single().Low); 121 | Assert.AreEqual(remotePortHigh, rule.RemotePorts.Ranges.Single().High); 122 | Assert.AreEqual(1, rule.RemoteAddresses.Ranges.Count); 123 | Assert.AreEqual(remoteAddressLow, rule.RemoteAddresses.Ranges.Single().Low); 124 | Assert.AreEqual(remoteAddressHigh, rule.RemoteAddresses.Ranges.Single().High); 125 | Assert.AreEqual(17, rule.Protocol.ProtocolNumber); 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /FirewallAnalysis.Tests/WindowsFirewallRuleTest.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.FirewallAnalysis.Tests 6 | { 7 | using System.Net; 8 | using Microsoft.FirewallAnalysis; 9 | using Microsoft.VisualStudio.TestTools.UnitTesting; 10 | using Microsoft.Z3; 11 | 12 | /// 13 | /// Unit tests for the class. 14 | /// 15 | [TestClass] 16 | public class WindowsFirewallRuleTest 17 | { 18 | /// 19 | /// Tests Z3 correctly binds to a single possible match for the rule. 20 | /// 21 | [TestMethod] 22 | public void TestSingle() 23 | { 24 | using (var ctx = new Context()) 25 | { 26 | int localPort = 80; 27 | IPAddress remoteAddress = IPAddress.Parse("192.168.1.1"); 28 | int remotePort = 128; 29 | int protocol = 6; 30 | string record = $"X\tYes\tAllow\t{localPort}\t{remoteAddress}\t{remotePort}\t{protocol}"; 31 | WindowsFirewallRule rule = WindowsFirewallRuleParser.ParseRecord( 32 | WindowsFirewallRuleParserTest.HeaderIndex, 33 | record, 34 | '\t'); 35 | var packetVars = new WindowsFirewallPacketVariables(ctx); 36 | Solver s = ctx.MkSolver(); 37 | s.Assert(rule.Matches(ctx, packetVars)); 38 | Status result = s.Check(); 39 | Assert.AreEqual(Status.SATISFIABLE, result); 40 | var packet = new WindowsFirewallPacket(s.Model); 41 | Assert.AreEqual(remoteAddress, packet.SourceAddress); 42 | Assert.AreEqual(remotePort, packet.SourcePort); 43 | Assert.AreEqual(localPort, packet.DestinationPort); 44 | Assert.AreEqual(protocol, packet.Protocol); 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /FirewallAnalysis.Tests/WindowsFirewallTest.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.FirewallAnalysis.Tests 6 | { 7 | using System.Linq; 8 | using System.Net; 9 | using Microsoft.FirewallAnalysis; 10 | using Microsoft.VisualStudio.TestTools.UnitTesting; 11 | using Microsoft.Z3; 12 | 13 | /// 14 | /// Unit tests for the class. 15 | /// 16 | [TestClass] 17 | public class WindowsFirewallTest 18 | { 19 | /// 20 | /// Tests a firewall allowing only a single packet. 21 | /// 22 | [TestMethod] 23 | public void TestAllowSingle() 24 | { 25 | using (var ctx = new Context()) 26 | { 27 | int localPort = 80; 28 | IPAddress remoteAddress = IPAddress.Parse("192.168.1.1"); 29 | int remotePort = 128; 30 | int protocol = 6; 31 | string record = $"X\tYes\tAllow\t{localPort}\t{remoteAddress}\t{remotePort}\t{protocol}"; 32 | string text = $"{WindowsFirewallRuleParserTest.HeaderText}\n{record}"; 33 | var firewall = new WindowsFirewall 34 | { 35 | BlockByDefault = true, 36 | Rules = WindowsFirewallRuleParser.Parse(text, '\t').ToList() 37 | }; 38 | 39 | var packetVars = new WindowsFirewallPacketVariables(ctx); 40 | Solver s = ctx.MkSolver(); 41 | s.Assert(firewall.Allows(ctx, packetVars)); 42 | Status result = s.Check(); 43 | Assert.AreEqual(Status.SATISFIABLE, result); 44 | var packet = new WindowsFirewallPacket(s.Model); 45 | Assert.AreEqual(remoteAddress, packet.SourceAddress); 46 | Assert.AreEqual(remotePort, packet.SourcePort); 47 | Assert.AreEqual(localPort, packet.DestinationPort); 48 | Assert.AreEqual(protocol, packet.Protocol); 49 | } 50 | } 51 | 52 | /// 53 | /// Tests firewall correctly handles conflicts; block rule wins. 54 | /// 55 | [TestMethod] 56 | public void TestConflict() 57 | { 58 | using (var ctx = new Context()) 59 | { 60 | int localPort = 80; 61 | IPAddress remoteAddress = IPAddress.Parse("192.168.1.1"); 62 | int remotePort = 128; 63 | int protocol = 6; 64 | string allowRecord = $"X\tYes\tAllow\t{localPort}\t{remoteAddress}\t{remotePort}\t{protocol}"; 65 | string blockRecord = $"Y\tYes\tBlock\t{localPort}\t{remoteAddress}\t{remotePort}\t{protocol}"; 66 | string text = $"{WindowsFirewallRuleParserTest.HeaderText}\n{allowRecord}\n{blockRecord}"; 67 | var firewall = new WindowsFirewall 68 | { 69 | BlockByDefault = true, 70 | Rules = WindowsFirewallRuleParser.Parse(text, '\t').ToList() 71 | }; 72 | 73 | var packetVars = new WindowsFirewallPacketVariables(ctx); 74 | Solver s = ctx.MkSolver(); 75 | s.Assert(firewall.Allows(ctx, packetVars)); 76 | Status result = s.Check(); 77 | Assert.AreEqual(Status.UNSATISFIABLE, result); 78 | } 79 | } 80 | 81 | /// 82 | /// Tests allow-by-default functionality. 83 | /// 84 | [TestMethod] 85 | public void TestAllowByDefault() 86 | { 87 | using (var ctx = new Context()) 88 | { 89 | IPAddress allowed = IPAddress.Parse("128.0.0.1"); 90 | string lowerBlock = "X\tYes\tBlock\tAny\t0.0.0.0-128.0.0.0\tAny\tAny"; 91 | string upperBlock = "Y\tYes\tBlock\tAny\t128.0.0.2-255.255.255.255\tAny\tAny"; 92 | string text = $"{WindowsFirewallRuleParserTest.HeaderText}\n{lowerBlock}\n{upperBlock}"; 93 | var firewall = new WindowsFirewall 94 | { 95 | BlockByDefault = false, 96 | Rules = WindowsFirewallRuleParser.Parse(text, '\t').ToList() 97 | }; 98 | 99 | var packetVars = new WindowsFirewallPacketVariables(ctx); 100 | Solver s = ctx.MkSolver(); 101 | s.Assert(firewall.Allows(ctx, packetVars)); 102 | Status result = s.Check(); 103 | Assert.AreEqual(Status.SATISFIABLE, result); 104 | var packet = new WindowsFirewallPacket(s.Model); 105 | Assert.AreEqual(allowed, packet.SourceAddress); 106 | Assert.AreEqual(null, packet.SourcePort); 107 | Assert.AreEqual(null, packet.DestinationPort); 108 | Assert.AreEqual(null, packet.Protocol); 109 | } 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /FirewallAnalysis/AddressRange.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.FirewallAnalysis 6 | { 7 | using System; 8 | using System.Linq; 9 | using System.Net; 10 | using Microsoft.Z3; 11 | 12 | /// 13 | /// A range of IP addresses, as commonly found in firewall rules. 14 | /// 15 | public class AddressRange 16 | { 17 | /// 18 | /// Gets or sets the lower bound of this IP range, inclusive. 19 | /// 20 | public IPAddress Low { get; set; } 21 | 22 | /// 23 | /// Gets or sets the upper bound of this IP range, inclusive. 24 | /// 25 | public IPAddress High { get; set; } 26 | 27 | /// 28 | /// Converts to bit vector understood by Z3. 29 | /// 30 | /// The Z3 context. 31 | /// The address to convert. 32 | /// A bit vector representation of the address. 33 | public static BitVecExpr AddressToBitVecExpr(Context ctx, IPAddress address) 34 | { 35 | // IPAddress.GetAddressBytes() function always returns bytes in network byte order (big-endian). 36 | // If this system is little-endian, we must reverse the bytes returned from IPAddress.GetAddressBytes(). 37 | byte[] addressBytes = BitConverter.IsLittleEndian 38 | ? address.GetAddressBytes().Reverse().ToArray() 39 | : address.GetAddressBytes(); 40 | uint addressAsUint = BitConverter.ToUInt32(addressBytes, 0); 41 | return ctx.MkNumeral(addressAsUint, ctx.MkBitVecSort(32)) as BitVecExpr; 42 | } 43 | 44 | /// 45 | /// Builds a boolean expression over the free variable which is true only if the 46 | /// variable, when bound, represent an IP address contained in this range. 47 | /// 48 | /// The Z3 context. 49 | /// The address variable to check for inclusion in the range. 50 | /// A Z3 boolean expression. 51 | public BoolExpr Contains(Context ctx, BitVecExpr address) 52 | { 53 | BoolExpr aboveLow = ctx.MkBVUGE(address, AddressRange.AddressToBitVecExpr(ctx, this.Low)); 54 | BoolExpr belowHigh = ctx.MkBVULE(address, AddressRange.AddressToBitVecExpr(ctx, this.High)); 55 | return ctx.MkAnd(aboveLow, belowHigh); 56 | } 57 | } 58 | } 59 | 60 | -------------------------------------------------------------------------------- /FirewallAnalysis/AddressSet.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.FirewallAnalysis 6 | { 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Linq; 10 | using Microsoft.Z3; 11 | 12 | /// 13 | /// A set of IP addresses. 14 | /// 15 | public class AddressSet 16 | { 17 | /// 18 | /// Gets or sets whether this address set contains all valid IP addresses. 19 | /// 20 | public bool ContainsAll { get; set; } 21 | 22 | /// 23 | /// Gets or sets the list of comprising this address set. 24 | /// 25 | public List Ranges { get; set; } 26 | 27 | /// 28 | /// Builds a boolean expression over the address variable which is true only if 29 | /// the address variable value, once bound, is contained in this address set. 30 | /// 31 | /// The Z3 context. 32 | /// The address variable to check for inclusion. 33 | /// A Z3 boolean expression. 34 | public BoolExpr Contains(Context ctx, BitVecExpr address) 35 | { 36 | if (this.ContainsAll) 37 | { 38 | return ctx.MkTrue(); 39 | } 40 | 41 | return ctx.MkOr(this.Ranges.Select(range => range.Contains(ctx, address)).ToArray()); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /FirewallAnalysis/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | // 4 | 5 | using System.Runtime.CompilerServices; 6 | 7 | [assembly: InternalsVisibleTo("Microsoft.FirewallAnalysis.Tests")] 8 | -------------------------------------------------------------------------------- /FirewallAnalysis/FirewallAnalysis.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Microsoft.FirewallAnalysis 5 | netstandard2.0 6 | x64 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /FirewallAnalysis/NetworkProtocol.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.FirewallAnalysis 6 | { 7 | using System; 8 | using System.Collections.Generic; 9 | using Microsoft.Z3; 10 | 11 | /// 12 | /// A network protocol specified in the 8-bit protocol field of an IPv4 packet header. 13 | /// 14 | /// 15 | public class NetworkProtocol 16 | { 17 | /// 18 | /// Gets or sets a value indicating whether this matches any protocol. 19 | /// 20 | public bool Any { get; set; } 21 | 22 | /// 23 | /// Gets or sets the ID of this network protocol. 24 | /// 25 | public int ProtocolNumber { get; set; } 26 | 27 | /// 28 | /// Converts a protocol number to a bit vector expression understood by Z3. 29 | /// 30 | /// The Z3 context. 31 | /// The protocol number to convert. 32 | /// A bit vector representation of the protocol number. 33 | public static BitVecExpr ProtocolToBitVecExpr(Context ctx, int protocolNumber) 34 | { 35 | return ctx.MkNumeral(protocolNumber, ctx.MkBitVecSort(8)) as BitVecExpr; 36 | } 37 | 38 | /// 39 | /// Returns the human-readable IANA standard name of some common protocol types. 40 | /// 41 | /// The protocol number for which to find a name. 42 | /// The protocol name. 43 | public static string GetProtocolName(int protocolNumber) 44 | { 45 | // Numbers from http://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml 46 | var protocolMap = new Dictionary 47 | { 48 | { 0, "HOPOPT" }, 49 | { 1, "ICMP" }, 50 | { 2, "IGMP" }, 51 | { 6, "TCP" }, 52 | { 17, "UDP" }, 53 | { 41, "IPv6" }, 54 | { 43, "IPv6-Route" }, 55 | { 44, "IPv6-Frag" }, 56 | { 47, "GRE" }, 57 | { 58, "IPv6-ICMP" }, 58 | { 59, "IPv6-NoNxt" }, 59 | { 60, "IPv6-Opts" }, 60 | { 112, "VRRP" }, 61 | { 113, "PGM" }, 62 | { 115, "L2TP" } 63 | }; 64 | 65 | string protocolName; 66 | if (!protocolMap.TryGetValue(protocolNumber, out protocolName)) 67 | { 68 | protocolName = protocolNumber.ToString(); 69 | } 70 | 71 | return protocolName; 72 | } 73 | 74 | /// 75 | /// Given a protocol name, attempts to find the IANA standard protocol number. 76 | /// 77 | /// The network protocol name. 78 | /// The network protocol number. 79 | /// Whether number resolution succeeded. 80 | public static bool TryGetProtocolNumber(string protocolName, out int protocolNumber) 81 | { 82 | // Numbers from http://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml 83 | var protocolMap = new Dictionary 84 | { 85 | { "HOPOPT", 0 }, 86 | { "ICMP", 1 }, 87 | { "ICMPv4", 1 }, // Note: name specific to Windows Firewall 88 | { "IGMP", 2 }, 89 | { "TCP", 6 }, 90 | { "UDP", 17 }, 91 | { "IPv6", 41 }, 92 | { "IPv6-Route", 43 }, 93 | { "IPv6-Frag", 44 }, 94 | { "GRE", 47 }, 95 | { "IPv6-ICMP", 58 }, 96 | { "ICMPv6", 58 }, // Note: name specific to Windows Firewall 97 | { "IPv6-NoNxt", 59 }, 98 | { "IPv6-Opts", 60 }, 99 | { "VRRP", 112 }, 100 | { "PGM", 113 }, 101 | { "L2TP", 115 } 102 | }; 103 | 104 | return protocolMap.TryGetValue(protocolName, out protocolNumber); 105 | } 106 | 107 | /// 108 | /// Builds a boolean expression over the port variable which is true only if 109 | /// the protocol variable value, once bound, matches this protocol. 110 | /// 111 | /// The Z3 context. 112 | /// The protocol variable to check for match. 113 | /// A Z3 boolean expression. 114 | public BoolExpr Matches(Context ctx, BitVecExpr protocol) 115 | { 116 | if (this.Any) 117 | { 118 | return ctx.MkTrue(); 119 | } 120 | 121 | return ctx.MkEq(protocol, NetworkProtocol.ProtocolToBitVecExpr(ctx, this.ProtocolNumber)); 122 | } 123 | } 124 | } 125 | 126 | -------------------------------------------------------------------------------- /FirewallAnalysis/PortRange.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.FirewallAnalysis 6 | { 7 | using System; 8 | using Microsoft.Z3; 9 | 10 | /// 11 | /// A range of ports, as commonly found in firewall rules. 12 | /// 13 | public class PortRange 14 | { 15 | /// 16 | /// Gets or sets the lower bound of this port range, inclusive. 17 | /// 18 | public int Low { get; set; } 19 | 20 | /// 21 | /// Gets or sets the upper bound of this port range, inclusive. 22 | /// 23 | public int High { get; set; } 24 | 25 | /// 26 | /// Converts port value to a bit vector expression understood by Z3. 27 | /// 28 | /// The Z3 context. 29 | /// The port value to convert. 30 | /// A bit vector expression understood by Z3. 31 | public static BitVecExpr PortToBitVecExpr(Context ctx, int port) 32 | { 33 | return ctx.MkNumeral(port, ctx.MkBitVecSort(16)) as BitVecExpr; 34 | } 35 | 36 | /// 37 | /// Builds a boolean expression over the free variable port, which is true 38 | /// only if port, when bound, is contained within this port range. 39 | /// 40 | /// The Z3 context. 41 | /// The port to check for inclusion in this range. 42 | /// A Z3 boolean expression. 43 | public BoolExpr Contains(Context ctx, BitVecExpr port) 44 | { 45 | BoolExpr aboveLow = ctx.MkBVUGE(port, PortRange.PortToBitVecExpr(ctx, this.Low)); 46 | BoolExpr belowHigh = ctx.MkBVULE(port, PortRange.PortToBitVecExpr(ctx, this.High)); 47 | return ctx.MkAnd(aboveLow, belowHigh); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /FirewallAnalysis/PortSet.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.FirewallAnalysis 6 | { 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Linq; 10 | using Microsoft.Z3; 11 | 12 | /// 13 | /// A set of ports. 14 | /// 15 | public class PortSet 16 | { 17 | /// 18 | /// Gets or sets a value indicating whether this port set contains all valid ports. 19 | /// 20 | public bool ContainsAll { get; set; } 21 | 22 | /// 23 | /// Gets or sets the list of comprising this port set. 24 | /// 25 | public List Ranges { get; set; } 26 | 27 | /// 28 | /// Builds a boolean expression over the port variable which is true only if 29 | /// the port variable value, once bound, is contained in this port set. 30 | /// 31 | /// The Z3 context. 32 | /// The port variable to check for inclusion. 33 | /// A Z3 boolean expression. 34 | public BoolExpr Contains(Context ctx, BitVecExpr port) 35 | { 36 | if (this.ContainsAll) 37 | { 38 | return ctx.MkTrue(); 39 | } 40 | 41 | return ctx.MkOr(this.Ranges.Select(range => range.Contains(ctx, port)).ToArray()); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /FirewallAnalysis/RetrieveModelValue.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.FirewallAnalysis 6 | { 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Linq; 10 | using System.Net; 11 | using Microsoft.Z3; 12 | 13 | /// 14 | /// Utility class to retrieve CLR value types from the Z3 model. 15 | /// 16 | public static class RetrieveModelValue 17 | { 18 | /// 19 | /// Attempts retrieval of an integer value from the Z3 model. 20 | /// 21 | /// Name of the variable in the model. 22 | /// The Z3 model. 23 | /// The retrieved value. 24 | /// Whether retrieval was successful. 25 | public static bool TryRetrieveInteger(string variableName, Model m, out int value) 26 | { 27 | Dictionary exprMap = m.ConstDecls.ToDictionary(c => c.Name.ToString(), m.ConstInterp); 28 | Expr variable; 29 | if (!exprMap.TryGetValue(variableName, out variable)) 30 | { 31 | value = 0; 32 | return false; 33 | } 34 | 35 | if (!int.TryParse(variable.ToString(), out value)) 36 | { 37 | value = 0; 38 | return false; 39 | } 40 | 41 | return true; 42 | } 43 | 44 | /// 45 | /// Attempts retrieval of an IP address value from the Z3 model. 46 | /// 47 | /// Name of the variable in the model. 48 | /// The Z3 model. 49 | /// The retrieved value. 50 | /// Whether retrieval was successful. 51 | public static bool TryRetrieveAddress(string variableName, Model m, out IPAddress value) 52 | { 53 | Dictionary exprMap = m.ConstDecls.ToDictionary(c => c.Name.ToString(), m.ConstInterp); 54 | Expr variable; 55 | if (!exprMap.TryGetValue(variableName, out variable)) 56 | { 57 | value = IPAddress.None; 58 | return false; 59 | } 60 | 61 | if (!IPAddress.TryParse(variable.ToString(), out value)) 62 | { 63 | value = IPAddress.None; 64 | return false; 65 | } 66 | 67 | return true; 68 | } 69 | 70 | /// 71 | /// Attempts retrieval of a string value from the Z3 model. 72 | /// 73 | /// Name of the variable in the model. 74 | /// The Z3 model. 75 | /// The retrieved value. 76 | /// Whether retrieval was successful. 77 | public static bool TryRetrieveString(string variableName, Model m, out string value) 78 | { 79 | Dictionary exprMap = m.ConstDecls.ToDictionary(c => c.Name.ToString(), m.ConstInterp); 80 | Expr variable; 81 | if (!exprMap.TryGetValue(variableName, out variable)) 82 | { 83 | value = string.Empty; 84 | return false; 85 | } 86 | 87 | value = variable.ToString(); 88 | return true; 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /FirewallAnalysis/WindowsFirewall.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.FirewallAnalysis 6 | { 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Linq; 10 | using Microsoft.Z3; 11 | 12 | /// 13 | /// A set of and logic linking them into 14 | /// a full working firewall. 15 | /// 16 | public class WindowsFirewall 17 | { 18 | /// 19 | /// Gets or sets the name of this firewall. 20 | /// 21 | public string Name { get; set; } 22 | 23 | /// 24 | /// Gets or sets whether packets matching no rules are blocked by default. 25 | /// 26 | public bool BlockByDefault { get; set; } 27 | 28 | /// 29 | /// Gets or sets the list of 30 | /// 31 | public List Rules { get; set; } 32 | 33 | /// 34 | /// Builds a boolean expression over free variables which is true only if the 35 | /// variables, once bound, represent a packet accepted by this firewall. 36 | /// This firewall accepts a packet if it matches zero block rules. If the 37 | /// setting is true, packets matching 38 | /// zero allow rules are blocked; otherwise, they are allowed. 39 | /// 40 | /// The Z3 context. 41 | /// The packet to process through this firewall. 42 | /// A Z3 boolean expression. 43 | public BoolExpr Allows(Context ctx, WindowsFirewallPacketVariables packet) 44 | { 45 | IEnumerable blockRules = 46 | from rule in this.Rules 47 | where rule.Enabled 48 | where !rule.Allow 49 | select rule.Matches(ctx, packet); 50 | 51 | BoolExpr hasBlockRuleMatch = ctx.MkOr(blockRules.ToArray()); 52 | 53 | if (!this.BlockByDefault) 54 | { 55 | return ctx.MkNot(hasBlockRuleMatch); 56 | } 57 | 58 | IEnumerable allowRules = 59 | from rule in this.Rules 60 | where rule.Enabled 61 | where rule.Allow 62 | select rule.Matches(ctx, packet); 63 | 64 | BoolExpr hasAllowRuleMatch = ctx.MkOr(allowRules.ToArray()); 65 | 66 | return ctx.MkAnd(hasAllowRuleMatch, ctx.MkNot(hasBlockRuleMatch)); 67 | } 68 | 69 | /// 70 | /// Returns the list of rules matching a packet. 71 | /// 72 | /// The Z3 context. 73 | /// The packet variables over which an expression is created. 74 | /// A specific binding of the packet variables. 75 | /// A list of rules matching the packet variable binding. 76 | public IEnumerable GetMatches(Context ctx, WindowsFirewallPacketVariables packetVariables, Model model) 77 | { 78 | return from rule in this.Rules 79 | where rule.Enabled 80 | where model.Eval(rule.Matches(ctx, packetVariables)).IsTrue 81 | select rule; 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /FirewallAnalysis/WindowsFirewallEquivalenceCheck.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.FirewallAnalysis 6 | { 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Linq; 10 | using Microsoft.Z3; 11 | 12 | /// 13 | /// Checks whether two instances are equivalent. 14 | /// 15 | public class WindowsFirewallEquivalenceCheck 16 | { 17 | /// 18 | /// Checks equivalence of two instances. 19 | /// 20 | /// First firewall. 21 | /// Second firewall. 22 | /// A report detailing firewall differences, if any. 23 | public static IEnumerable CheckEquivalence(WindowsFirewall f1, WindowsFirewall f2) 24 | { 25 | var pastInconsistencies = new List(); 26 | using (var ctx = new Context()) 27 | { 28 | while (true) 29 | { 30 | Solver s = ctx.MkSolver(); 31 | var packetVars = new WindowsFirewallPacketVariables(ctx); 32 | BoolExpr firewallInequivalence = ctx.MkNot(ctx.MkIff(f1.Allows(ctx, packetVars), f2.Allows(ctx, packetVars))); 33 | BoolExpr matchesPastInconsistency = ctx.MkOr(pastInconsistencies.Select(p => p.Matches(ctx, packetVars)).ToArray()); 34 | s.Assert(ctx.MkAnd(firewallInequivalence, ctx.MkNot(matchesPastInconsistency))); 35 | if (Status.UNSATISFIABLE == s.Check()) 36 | { 37 | break; 38 | } 39 | 40 | Model m = s.Model; 41 | var packet = new WindowsFirewallPacket(m); 42 | pastInconsistencies.Add(packet); 43 | var inconsistency = new WindowsFirewallInconsistency 44 | { 45 | Packet = packet, 46 | Firewalls = Tuple.Create(f1, f2), 47 | Allowed = 48 | Tuple.Create(m.Eval(f1.Allows(ctx, packetVars)).IsTrue, m.Eval(f2.Allows(ctx, packetVars)).IsTrue), 49 | RuleMatches = 50 | Tuple.Create(f1.GetMatches(ctx, packetVars, m).ToList(), f2.GetMatches(ctx, packetVars, m).ToList()) 51 | }; 52 | 53 | yield return inconsistency; 54 | } 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /FirewallAnalysis/WindowsFirewallInconsistency.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.FirewallAnalysis 6 | { 7 | using System; 8 | using System.Collections.Generic; 9 | 10 | /// 11 | /// Report of a single inconsistency between two intances. 12 | /// 13 | public class WindowsFirewallInconsistency 14 | { 15 | /// 16 | /// Gets or sets the inconsistently-handled . 17 | /// 18 | public WindowsFirewallPacket Packet { get; set; } 19 | 20 | /// 21 | /// Gets or sets the instances. 22 | /// 23 | public Tuple Firewalls { get; set; } 24 | 25 | /// 26 | /// Gets or sets whether each allowed the packet. 27 | /// 28 | public Tuple Allowed { get; set; } 29 | 30 | /// 31 | /// Gets or sets the rules matched by the . 32 | /// 33 | public Tuple, List> RuleMatches { get; set; } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /FirewallAnalysis/WindowsFirewallPacket.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.FirewallAnalysis 6 | { 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Net; 10 | using Microsoft.Z3; 11 | 12 | /// 13 | /// Values found in a typical packet examined by Windows Firewall. 14 | /// 15 | public class WindowsFirewallPacket 16 | { 17 | /// 18 | /// Initializes a new instance of the class. 19 | /// 20 | public WindowsFirewallPacket() 21 | { 22 | this.SourceAddress = null; 23 | this.SourcePort = null; 24 | this.DestinationPort = null; 25 | this.Protocol = null; 26 | } 27 | 28 | /// 29 | /// Initializes a new instance of the class 30 | /// from the variables in the model. 31 | /// 32 | /// A Z3 model. 33 | public WindowsFirewallPacket(Model m) 34 | { 35 | IPAddress sourceAddress; 36 | this.SourceAddress = RetrieveModelValue.TryRetrieveAddress(WindowsFirewallPacketVariables.SourceAddressVariableName, m, out sourceAddress) 37 | ? sourceAddress 38 | : null; 39 | 40 | int sourcePort; 41 | this.SourcePort = RetrieveModelValue.TryRetrieveInteger(WindowsFirewallPacketVariables.SourcePortVariableName, m, out sourcePort) 42 | ? (int?)sourcePort 43 | : null; 44 | 45 | int destinationPort; 46 | this.DestinationPort = RetrieveModelValue.TryRetrieveInteger(WindowsFirewallPacketVariables.DestinationPortVariableName, m, out destinationPort) 47 | ? (int?)destinationPort 48 | : null; 49 | 50 | int protocol; 51 | this.Protocol = RetrieveModelValue.TryRetrieveInteger(WindowsFirewallPacketVariables.ProtocolVariableName, m, out protocol) 52 | ? (int?)protocol 53 | : null; 54 | } 55 | 56 | /// 57 | /// Gets or sets the source address of this packet. 58 | /// 59 | public IPAddress SourceAddress { get; set; } 60 | 61 | /// 62 | /// Gets or sets the source port of this packet. 63 | /// 64 | public int? SourcePort { get; set; } 65 | 66 | /// 67 | /// Gets or sets the destination port of this packet. 68 | /// 69 | public int? DestinationPort { get; set; } 70 | 71 | /// 72 | /// Gets or sets the protocol of this packet. 73 | /// 74 | public int? Protocol { get; set; } 75 | 76 | /// 77 | /// Builds a string representation of this packet. 78 | /// 79 | /// A string representation of this packet. 80 | public new string ToString() 81 | { 82 | return $"Src Address: {this.SourceAddress?.ToString() ?? "Any"} " 83 | + $"| Src Port: {this.SourcePort?.ToString() ?? "Any"} " 84 | + $"| Dest Port: {this.DestinationPort?.ToString() ?? "Any"} " 85 | + $"| Protocol: {this.Protocol?.ToString() ?? "Any"}"; 86 | } 87 | 88 | /// 89 | /// Creates a boolean expression expressing the conditions under which the given packet 90 | /// variables match this specific packet. 91 | /// 92 | /// The Z3 context. 93 | /// The packet variables over which to form an expression. 94 | /// A boolean expression. 95 | public BoolExpr Matches(Context ctx, WindowsFirewallPacketVariables packet) 96 | { 97 | var conjuncts = new List(); 98 | if (this.SourceAddress != null) 99 | { 100 | conjuncts.Add(ctx.MkEq(packet.SourceAddress, AddressRange.AddressToBitVecExpr(ctx, this.SourceAddress))); 101 | } 102 | 103 | if (this.SourcePort != null) 104 | { 105 | conjuncts.Add(ctx.MkEq(packet.SourcePort, PortRange.PortToBitVecExpr(ctx, (int)this.SourcePort))); 106 | } 107 | 108 | if (this.DestinationPort != null) 109 | { 110 | conjuncts.Add(ctx.MkEq(packet.DestinationPort, PortRange.PortToBitVecExpr(ctx, (int)this.DestinationPort))); 111 | } 112 | 113 | if (this.Protocol != null) 114 | { 115 | conjuncts.Add(ctx.MkEq(packet.Protocol, NetworkProtocol.ProtocolToBitVecExpr(ctx, (int)this.Protocol))); 116 | } 117 | 118 | return ctx.MkAnd(conjuncts.ToArray()); 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /FirewallAnalysis/WindowsFirewallPacketVariables.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.FirewallAnalysis 6 | { 7 | using System; 8 | using Microsoft.Z3; 9 | 10 | /// 11 | /// A packet to be evaluated for match with . 12 | /// 13 | public class WindowsFirewallPacketVariables 14 | { 15 | /// 16 | /// Name of the source address variable in Z3. 17 | /// 18 | internal const string SourceAddressVariableName = "sourceAddress"; 19 | 20 | /// 21 | /// Name of the source port variable in Z3. 22 | /// 23 | internal const string SourcePortVariableName = "sourcePort"; 24 | 25 | /// 26 | /// Name of the destination port variable in Z3. 27 | /// 28 | internal const string DestinationPortVariableName = "destinationPort"; 29 | 30 | /// 31 | /// Name of the protocol variable in Z3. 32 | /// 33 | internal const string ProtocolVariableName = "protocol"; 34 | 35 | /// 36 | /// Initializes a new instance of the class. 37 | /// 38 | /// The Z3 context. 39 | public WindowsFirewallPacketVariables(Context ctx) 40 | { 41 | this.SourceAddress = ctx.MkConst(WindowsFirewallPacketVariables.SourceAddressVariableName, ctx.MkBitVecSort(32)) as BitVecExpr; 42 | this.SourcePort = ctx.MkConst(WindowsFirewallPacketVariables.SourcePortVariableName, ctx.MkBitVecSort(16)) as BitVecExpr; 43 | this.DestinationPort = ctx.MkConst(WindowsFirewallPacketVariables.DestinationPortVariableName, ctx.MkBitVecSort(16)) as BitVecExpr; 44 | this.Protocol = ctx.MkConst(WindowsFirewallPacketVariables.ProtocolVariableName, ctx.MkBitVecSort(8)) as BitVecExpr; 45 | } 46 | 47 | /// 48 | /// Gets or sets the source IPv4 address of this packet. 49 | /// 50 | public BitVecExpr SourceAddress { get; set; } 51 | 52 | /// 53 | /// Gets or sets the source port of this packet. 54 | /// 55 | public BitVecExpr SourcePort { get; set; } 56 | 57 | /// 58 | /// Gets or sets the destination port of this packet. 59 | /// 60 | public BitVecExpr DestinationPort { get; set; } 61 | 62 | /// 63 | /// Gets or sets the protocol of this packet. 64 | /// 65 | public BitVecExpr Protocol { get; set; } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /FirewallAnalysis/WindowsFirewallRule.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.FirewallAnalysis 6 | { 7 | using System; 8 | using Microsoft.Z3; 9 | 10 | /// 11 | /// Represents a single Windows Firewall rule. 12 | /// 13 | public class WindowsFirewallRule 14 | { 15 | /// 16 | /// Gets or sets the name of this firewall rule. 17 | /// 18 | public string Name { get; set; } 19 | 20 | /// 21 | /// Gets or sets remote addresses matched by this rule. 22 | /// 23 | public AddressSet RemoteAddresses { get; set; } 24 | 25 | /// 26 | /// Gets or sets the remote ports matched by this rule. 27 | /// 28 | public PortSet RemotePorts { get; set; } 29 | 30 | /// 31 | /// Gets or sets the local ports matched by this rule. 32 | /// 33 | public PortSet LocalPorts { get; set; } 34 | 35 | /// 36 | /// Gets or sets the protocol matched by this rule. 37 | /// 38 | public NetworkProtocol Protocol { get; set; } 39 | 40 | /// 41 | /// Gets or sets whether this rule is enabled. 42 | /// 43 | public bool Enabled { get; set; } 44 | 45 | /// 46 | /// Gets or sets whether this rule allows a matching packet. 47 | /// 48 | public bool Allow { get; set; } 49 | 50 | /// 51 | /// Builds a boolean expression over free variables which is true only if the 52 | /// variables, once bound, represent a packet matching this rule. 53 | /// 54 | /// The Z3 context. 55 | /// The packet to match against this rule. 56 | /// A Z3 boolean expression. 57 | public BoolExpr Matches(Context ctx, WindowsFirewallPacketVariables packet) 58 | { 59 | BoolExpr[] conjuncts = 60 | { 61 | this.RemoteAddresses.Contains(ctx, packet.SourceAddress), 62 | this.RemotePorts.Contains(ctx, packet.SourcePort), 63 | this.LocalPorts.Contains(ctx, packet.DestinationPort), 64 | this.Protocol.Matches(ctx, packet.Protocol) 65 | }; 66 | 67 | return ctx.MkAnd(conjuncts); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /FirewallAnalysis/WindowsFirewallRuleParser.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.FirewallAnalysis 6 | { 7 | using System; 8 | using System.Collections.Generic; 9 | using System.IO; 10 | using System.Linq; 11 | using System.Net; 12 | using System.Net.Sockets; 13 | 14 | /// 15 | /// Parses Windows Firewall rule dump into list. 16 | /// 17 | public static class WindowsFirewallRuleParser 18 | { 19 | /// 20 | /// Header name for name of rule. 21 | /// 22 | internal const string RuleNameName = "Name"; 23 | 24 | /// 25 | /// Header name for whether rule is enabled. 26 | /// 27 | internal const string EnabledHeaderName = "Enabled"; 28 | 29 | /// 30 | /// Header name for action of the rule. 31 | /// 32 | internal const string PermissionHeaderName = "Action"; 33 | 34 | /// 35 | /// Header name for the local port. 36 | /// 37 | internal const string LocalPortHeaderName = "Local Port"; 38 | 39 | /// 40 | /// Header name for the remote address. 41 | /// 42 | internal const string RemoteAddressHeaderName = "Remote Address"; 43 | 44 | /// 45 | /// Header name for the remote port. 46 | /// 47 | internal const string RemotePortHeaderName = "Remote Port"; 48 | 49 | /// 50 | /// Header name for the protocol. 51 | /// 52 | internal const string ProtocolHeaderName = "Protocol"; 53 | 54 | /// 55 | /// Gets all headers required to be present in the text. 56 | /// 57 | internal static string[] RequiredHeaders { get; } = 58 | { 59 | WindowsFirewallRuleParser.RuleNameName, 60 | WindowsFirewallRuleParser.EnabledHeaderName, 61 | WindowsFirewallRuleParser.PermissionHeaderName, 62 | WindowsFirewallRuleParser.LocalPortHeaderName, 63 | WindowsFirewallRuleParser.RemoteAddressHeaderName, 64 | WindowsFirewallRuleParser.RemotePortHeaderName, 65 | WindowsFirewallRuleParser.ProtocolHeaderName 66 | }; 67 | 68 | /// 69 | /// Parses text from a Windows Firewall dump file into a list of . 70 | /// 71 | /// The text to parse. 72 | /// The character separating columns. 73 | /// A list of . 74 | public static IEnumerable Parse(string text, char separator) 75 | { 76 | using (var reader = new StringReader(text)) 77 | { 78 | Dictionary headerIndex = ParseHeader(WindowsFirewallRuleParser.RequiredHeaders, reader.ReadLine(), separator); 79 | 80 | string line = reader.ReadLine(); 81 | for (int i = 0; null != line; i++) 82 | { 83 | WindowsFirewallRule rule; 84 | try 85 | { 86 | rule = ParseRecord(headerIndex, line, separator); 87 | } 88 | catch (FormatException e) 89 | { 90 | Console.ForegroundColor = ConsoleColor.Yellow; 91 | Console.WriteLine($"Skipping line {i + 2} - {e.Message}"); 92 | Console.ResetColor(); 93 | line = reader.ReadLine(); 94 | continue; 95 | } 96 | 97 | yield return rule; 98 | 99 | line = reader.ReadLine(); 100 | } 101 | } 102 | } 103 | 104 | /// 105 | /// Parses and verifies the file header. 106 | /// 107 | /// Columns which are required to be present in the header. 108 | /// The unparsed header. 109 | /// The character separating columns. 110 | /// An index of column headers. 111 | public static Dictionary ParseHeader(string[] requiredHeaders, string headerLine, char separator) 112 | { 113 | if (string.IsNullOrEmpty(headerLine)) 114 | { 115 | throw new ArgumentNullException(nameof(headerLine)); 116 | } 117 | 118 | string[] allHeaders = headerLine.Split(new[] { separator }, StringSplitOptions.None); 119 | var headerIndex = new Dictionary(); 120 | for (int i = 0; i < allHeaders.Length; i++) 121 | { 122 | headerIndex[allHeaders[i].Trim()] = i; 123 | } 124 | 125 | string[] missing = requiredHeaders.Except(headerIndex.Keys).ToArray(); 126 | if (missing.Any()) 127 | { 128 | throw new FormatException($"Failed to find required headers: {string.Join(", ", missing)}"); 129 | } 130 | 131 | return headerIndex; 132 | } 133 | 134 | /// 135 | /// Parses and verifies a record. 136 | /// 137 | /// The column header index. 138 | /// The unparsed record. 139 | /// The character separating columns. 140 | /// An instance of . 141 | public static WindowsFirewallRule ParseRecord(Dictionary headerIndex, string recordLine, char separator) 142 | { 143 | if (string.IsNullOrEmpty(recordLine)) 144 | { 145 | throw new ArgumentNullException(recordLine); 146 | } 147 | 148 | string[] record = recordLine.Split(new[] { separator }, StringSplitOptions.None); 149 | return new WindowsFirewallRule 150 | { 151 | Name = WindowsFirewallRuleParser.ParseName(record[headerIndex[RuleNameName]]), 152 | RemoteAddresses = WindowsFirewallRuleParser.ParseAddressSet(record[headerIndex[RemoteAddressHeaderName]]), 153 | RemotePorts = WindowsFirewallRuleParser.ParsePortSet(record[headerIndex[RemotePortHeaderName]]), 154 | LocalPorts = WindowsFirewallRuleParser.ParsePortSet(record[headerIndex[LocalPortHeaderName]]), 155 | Protocol = WindowsFirewallRuleParser.ParseNetworkProtocol(record[headerIndex[ProtocolHeaderName]]), 156 | Enabled = WindowsFirewallRuleParser.ParseEnabled(record[headerIndex[EnabledHeaderName]]), 157 | Allow = WindowsFirewallRuleParser.ParseAction(record[headerIndex[PermissionHeaderName]]) 158 | }; 159 | } 160 | 161 | /// 162 | /// Parses the name of the rule. 163 | /// 164 | /// Unparsed text. 165 | /// The parsed name of the rule. 166 | private static string ParseName(string text) 167 | { 168 | return text.Trim(); 169 | } 170 | 171 | /// 172 | /// Parses the . 173 | /// 174 | /// Unparsed text. 175 | /// The parsed . 176 | private static AddressSet ParseAddressSet(string text) 177 | { 178 | string trimmed = text.Trim(); 179 | if ("Any" == trimmed) 180 | { 181 | return new AddressSet 182 | { 183 | ContainsAll = true 184 | }; 185 | } 186 | 187 | // Parse list of individual addresses and/or address ranges. 188 | // e.g. "127.0.0.1-127.0.0.10, 192.168.0.0-192.168.0.10, 255.255.255.255" 189 | List ranges = new List(); 190 | string[] split = trimmed.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); 191 | foreach (string unparsedRange in split) 192 | { 193 | string trimmedUnparsedRange = unparsedRange.Trim(); 194 | string[] rangeOrSingle = trimmedUnparsedRange.Split(new[] { '-' }, StringSplitOptions.RemoveEmptyEntries); 195 | if (1 == rangeOrSingle.Length) 196 | { 197 | IPAddress single = IPAddress.Parse(rangeOrSingle[0].Trim()); 198 | if (AddressFamily.InterNetworkV6 == single.AddressFamily) 199 | { 200 | throw new FormatException("IPv6 not supported."); 201 | } 202 | 203 | ranges.Add(new AddressRange 204 | { 205 | Low = single, 206 | High = single 207 | }); 208 | } 209 | else 210 | { 211 | var range = new AddressRange 212 | { 213 | Low = IPAddress.Parse(rangeOrSingle[0].Trim()), 214 | High = IPAddress.Parse(rangeOrSingle[1].Trim()) 215 | }; 216 | 217 | if (AddressFamily.InterNetworkV6 == range.Low.AddressFamily || 218 | AddressFamily.InterNetworkV6 == range.High.AddressFamily) 219 | { 220 | throw new FormatException("IPv6 not supported."); 221 | } 222 | 223 | ranges.Add(new AddressRange 224 | { 225 | Low = IPAddress.Parse(rangeOrSingle[0].Trim()), 226 | High = IPAddress.Parse(rangeOrSingle[1].Trim()) 227 | }); 228 | } 229 | } 230 | 231 | return new AddressSet 232 | { 233 | ContainsAll = false, 234 | Ranges = ranges 235 | }; 236 | } 237 | 238 | /// 239 | /// Parses the . 240 | /// 241 | /// Unparsed text. 242 | /// The parsed . 243 | private static PortSet ParsePortSet(string text) 244 | { 245 | string trimmed = text.Trim(); 246 | 247 | if (string.IsNullOrEmpty(trimmed)) 248 | { 249 | throw new FormatException("Port is null or empty."); 250 | } 251 | 252 | if ("Any" == trimmed) 253 | { 254 | return new PortSet 255 | { 256 | ContainsAll = true 257 | }; 258 | } 259 | 260 | // Port macros used by Windows Firewall. 261 | string[] macros = 262 | { 263 | "RPC Endpoint Mapper", 264 | "RPC Dynamic Ports", 265 | "IPHTTPS", 266 | "Edge Traversal", 267 | "PlayTo Discovery" 268 | }; 269 | 270 | if (macros.Contains(trimmed)) 271 | { 272 | throw new FormatException($"Port macros are not supported: {trimmed}"); 273 | } 274 | 275 | // Parse list of individual ports and/or port ranges. 276 | // e.g. "80, 8080, 20000-20008" 277 | List ranges = new List(); 278 | string[] split = trimmed.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); 279 | foreach (string unparsedRange in split) 280 | { 281 | string trimmedUnparsedRange = unparsedRange.Trim(); 282 | string[] rangeOrSingle = trimmedUnparsedRange.Split(new[] { '-' }, StringSplitOptions.RemoveEmptyEntries); 283 | if (1 == rangeOrSingle.Length) 284 | { 285 | int single = int.Parse(rangeOrSingle[0].Trim()); 286 | ranges.Add(new PortRange 287 | { 288 | Low = single, 289 | High = single 290 | }); 291 | } 292 | else 293 | { 294 | ranges.Add(new PortRange 295 | { 296 | Low = int.Parse(rangeOrSingle[0].Trim()), 297 | High = int.Parse(rangeOrSingle[1].Trim()) 298 | }); 299 | } 300 | } 301 | 302 | return new PortSet 303 | { 304 | ContainsAll = false, 305 | Ranges = ranges 306 | }; 307 | } 308 | 309 | /// 310 | /// Parses the . 311 | /// 312 | /// Unparsed text. 313 | /// The parsed . 314 | private static NetworkProtocol ParseNetworkProtocol(string text) 315 | { 316 | string trimmed = text.Trim(); 317 | if ("Any" == trimmed) 318 | { 319 | return new NetworkProtocol 320 | { 321 | Any = true 322 | }; 323 | } 324 | 325 | int protocolNumber; 326 | if (!NetworkProtocol.TryGetProtocolNumber(trimmed, out protocolNumber)) 327 | { 328 | protocolNumber = int.Parse(trimmed); 329 | } 330 | 331 | return new NetworkProtocol 332 | { 333 | Any = false, 334 | ProtocolNumber = protocolNumber 335 | }; 336 | } 337 | 338 | /// 339 | /// Parses whether the rule is enabled. 340 | /// 341 | /// Unparsed text. 342 | /// Whether the rule is enabled. 343 | private static bool ParseEnabled(string text) 344 | { 345 | return "Yes" == text.Trim(); 346 | } 347 | 348 | /// 349 | /// Parses whether the rule blocks or allows packets. 350 | /// 351 | /// Unparsed text. 352 | /// Whether the rule blocks or allows packets. 353 | private static bool ParseAction(string text) 354 | { 355 | string trimmed = text.Trim(); 356 | switch (trimmed) 357 | { 358 | case "Allow": 359 | return true; 360 | case "Block": 361 | return false; 362 | default: 363 | throw new FormatException($"Invalid rule action: {trimmed}"); 364 | } 365 | } 366 | } 367 | } 368 | -------------------------------------------------------------------------------- /FirewallChecker.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.31702.278 5 | MinimumVisualStudioVersion = 15.0.26124.0 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FirewallAnalysis", "FirewallAnalysis\FirewallAnalysis.csproj", "{ECF77030-3879-4B5D-8443-BAB92E66B22F}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FirewallAnalysis.Tests", "FirewallAnalysis.Tests\FirewallAnalysis.Tests.csproj", "{504CB30A-8536-45FA-B535-25EFC7EF397D}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FirewallEquivalenceCheckerCmd", "FirewallEquivalenceCheckerCmd\FirewallEquivalenceCheckerCmd.csproj", "{D3E4EFF3-6B27-4126-BA98-5043152046C9}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FirewallQueryCmd", "FirewallQueryCmd\FirewallQueryCmd.csproj", "{E1512823-EE90-4F17-9C67-43CA45F6B790}" 13 | EndProject 14 | Global 15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 16 | Debug|Any CPU = Debug|Any CPU 17 | Debug|x64 = Debug|x64 18 | Debug|x86 = Debug|x86 19 | Release|Any CPU = Release|Any CPU 20 | Release|x64 = Release|x64 21 | Release|x86 = Release|x86 22 | EndGlobalSection 23 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 24 | {ECF77030-3879-4B5D-8443-BAB92E66B22F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {ECF77030-3879-4B5D-8443-BAB92E66B22F}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {ECF77030-3879-4B5D-8443-BAB92E66B22F}.Debug|x64.ActiveCfg = Debug|Any CPU 27 | {ECF77030-3879-4B5D-8443-BAB92E66B22F}.Debug|x64.Build.0 = Debug|Any CPU 28 | {ECF77030-3879-4B5D-8443-BAB92E66B22F}.Debug|x86.ActiveCfg = Debug|Any CPU 29 | {ECF77030-3879-4B5D-8443-BAB92E66B22F}.Debug|x86.Build.0 = Debug|Any CPU 30 | {ECF77030-3879-4B5D-8443-BAB92E66B22F}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {ECF77030-3879-4B5D-8443-BAB92E66B22F}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {ECF77030-3879-4B5D-8443-BAB92E66B22F}.Release|x64.ActiveCfg = Release|Any CPU 33 | {ECF77030-3879-4B5D-8443-BAB92E66B22F}.Release|x64.Build.0 = Release|Any CPU 34 | {ECF77030-3879-4B5D-8443-BAB92E66B22F}.Release|x86.ActiveCfg = Release|Any CPU 35 | {ECF77030-3879-4B5D-8443-BAB92E66B22F}.Release|x86.Build.0 = Release|Any CPU 36 | {504CB30A-8536-45FA-B535-25EFC7EF397D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 37 | {504CB30A-8536-45FA-B535-25EFC7EF397D}.Debug|Any CPU.Build.0 = Debug|Any CPU 38 | {504CB30A-8536-45FA-B535-25EFC7EF397D}.Debug|x64.ActiveCfg = Debug|Any CPU 39 | {504CB30A-8536-45FA-B535-25EFC7EF397D}.Debug|x64.Build.0 = Debug|Any CPU 40 | {504CB30A-8536-45FA-B535-25EFC7EF397D}.Debug|x86.ActiveCfg = Debug|Any CPU 41 | {504CB30A-8536-45FA-B535-25EFC7EF397D}.Debug|x86.Build.0 = Debug|Any CPU 42 | {504CB30A-8536-45FA-B535-25EFC7EF397D}.Release|Any CPU.ActiveCfg = Release|Any CPU 43 | {504CB30A-8536-45FA-B535-25EFC7EF397D}.Release|Any CPU.Build.0 = Release|Any CPU 44 | {504CB30A-8536-45FA-B535-25EFC7EF397D}.Release|x64.ActiveCfg = Release|Any CPU 45 | {504CB30A-8536-45FA-B535-25EFC7EF397D}.Release|x64.Build.0 = Release|Any CPU 46 | {504CB30A-8536-45FA-B535-25EFC7EF397D}.Release|x86.ActiveCfg = Release|Any CPU 47 | {504CB30A-8536-45FA-B535-25EFC7EF397D}.Release|x86.Build.0 = Release|Any CPU 48 | {D3E4EFF3-6B27-4126-BA98-5043152046C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 49 | {D3E4EFF3-6B27-4126-BA98-5043152046C9}.Debug|Any CPU.Build.0 = Debug|Any CPU 50 | {D3E4EFF3-6B27-4126-BA98-5043152046C9}.Debug|x64.ActiveCfg = Debug|Any CPU 51 | {D3E4EFF3-6B27-4126-BA98-5043152046C9}.Debug|x64.Build.0 = Debug|Any CPU 52 | {D3E4EFF3-6B27-4126-BA98-5043152046C9}.Debug|x86.ActiveCfg = Debug|Any CPU 53 | {D3E4EFF3-6B27-4126-BA98-5043152046C9}.Debug|x86.Build.0 = Debug|Any CPU 54 | {D3E4EFF3-6B27-4126-BA98-5043152046C9}.Release|Any CPU.ActiveCfg = Release|Any CPU 55 | {D3E4EFF3-6B27-4126-BA98-5043152046C9}.Release|Any CPU.Build.0 = Release|Any CPU 56 | {D3E4EFF3-6B27-4126-BA98-5043152046C9}.Release|x64.ActiveCfg = Release|Any CPU 57 | {D3E4EFF3-6B27-4126-BA98-5043152046C9}.Release|x64.Build.0 = Release|Any CPU 58 | {D3E4EFF3-6B27-4126-BA98-5043152046C9}.Release|x86.ActiveCfg = Release|Any CPU 59 | {D3E4EFF3-6B27-4126-BA98-5043152046C9}.Release|x86.Build.0 = Release|Any CPU 60 | {E1512823-EE90-4F17-9C67-43CA45F6B790}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 61 | {E1512823-EE90-4F17-9C67-43CA45F6B790}.Debug|Any CPU.Build.0 = Debug|Any CPU 62 | {E1512823-EE90-4F17-9C67-43CA45F6B790}.Debug|x64.ActiveCfg = Debug|Any CPU 63 | {E1512823-EE90-4F17-9C67-43CA45F6B790}.Debug|x64.Build.0 = Debug|Any CPU 64 | {E1512823-EE90-4F17-9C67-43CA45F6B790}.Debug|x86.ActiveCfg = Debug|Any CPU 65 | {E1512823-EE90-4F17-9C67-43CA45F6B790}.Debug|x86.Build.0 = Debug|Any CPU 66 | {E1512823-EE90-4F17-9C67-43CA45F6B790}.Release|Any CPU.ActiveCfg = Release|Any CPU 67 | {E1512823-EE90-4F17-9C67-43CA45F6B790}.Release|Any CPU.Build.0 = Release|Any CPU 68 | {E1512823-EE90-4F17-9C67-43CA45F6B790}.Release|x64.ActiveCfg = Release|Any CPU 69 | {E1512823-EE90-4F17-9C67-43CA45F6B790}.Release|x64.Build.0 = Release|Any CPU 70 | {E1512823-EE90-4F17-9C67-43CA45F6B790}.Release|x86.ActiveCfg = Release|Any CPU 71 | {E1512823-EE90-4F17-9C67-43CA45F6B790}.Release|x86.Build.0 = Release|Any CPU 72 | EndGlobalSection 73 | GlobalSection(SolutionProperties) = preSolution 74 | HideSolutionNode = FALSE 75 | EndGlobalSection 76 | GlobalSection(ExtensibilityGlobals) = postSolution 77 | SolutionGuid = {E4CFE8EA-05F9-4360-8BB3-CD493EF78B52} 78 | EndGlobalSection 79 | EndGlobal 80 | -------------------------------------------------------------------------------- /FirewallEquivalenceCheckerCmd/FirewallEquivalenceCheckerCmd.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | Exe 13 | netcoreapp5.0 14 | x64 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /FirewallEquivalenceCheckerCmd/Options.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.FirewallEquivalenceCheckerCmd 6 | { 7 | using CommandLine; 8 | 9 | /// 10 | /// Command line options handler. 11 | /// 12 | internal class Options 13 | { 14 | /// 15 | /// Gets or sets path to first firewall rule file. 16 | /// 17 | [Option("firewall1", Required = true, HelpText = "Path to first tab-separated firewall rule file.")] 18 | public string Firewall1Filepath { get; set; } 19 | 20 | /// 21 | /// Gets or sets path to second firewall rule file. 22 | /// 23 | [Option("firewall2", Required = true, HelpText = "Path to second tab-separated firewall rule file.")] 24 | public string Firewall2Filepath { get; set; } 25 | 26 | /// 27 | /// Gets or sets a value indicating whether second firewall blocks packets by default. 28 | /// 29 | [Option("blockByDefault1", Required = false, Default = true, HelpText = "Whether first firewall blocks packets by default.")] 30 | public bool Firewall1BlockByDefault { get; set; } 31 | 32 | /// 33 | /// Gets or sets a value indicating whether second firewall blocks packets by default. 34 | /// 35 | [Option("blockByDefault2", Required = false, Default = true, HelpText = "Whether second firewall blocks packets by default.")] 36 | public bool Firewall2BlockByDefault { get; set; } 37 | 38 | /// 39 | /// Gets or sets the number of inconsistencies to find. 40 | /// 41 | [Option("inconsistencyCount", Required = false, Default = 10, HelpText = "Number of inconsistencies to find.")] 42 | public int InconsistencyCount { get; set; } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /FirewallEquivalenceCheckerCmd/Program.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.FirewallEquivalenceCheckerCmd 6 | { 7 | using System; 8 | using System.Collections.Generic; 9 | using System.IO; 10 | using System.Linq; 11 | using CommandLine; 12 | using Microsoft.FirewallAnalysis; 13 | 14 | /// 15 | /// Main entry point to checker command line tool. 16 | /// 17 | internal class Program 18 | { 19 | /// 20 | /// Returns a list of inconsistencies between two firewall files. 21 | /// 22 | /// Options controlling the check. 23 | /// A list of firewall inconsistencies. 24 | public static List CheckFirewalls(Options options) 25 | { 26 | Console.WriteLine("Parsing first firewall..."); 27 | WindowsFirewall f1 = new WindowsFirewall 28 | { 29 | BlockByDefault = options.Firewall1BlockByDefault, 30 | Rules = WindowsFirewallRuleParser.Parse(File.ReadAllText(options.Firewall1Filepath), '\t').ToList() 31 | }; 32 | 33 | Console.WriteLine("Parsing second firewall..."); 34 | WindowsFirewall f2 = new WindowsFirewall 35 | { 36 | BlockByDefault = options.Firewall2BlockByDefault, 37 | Rules = WindowsFirewallRuleParser.Parse(File.ReadAllText(options.Firewall2Filepath), '\t').ToList() 38 | }; 39 | 40 | Console.WriteLine("Running equivalence check..."); 41 | return WindowsFirewallEquivalenceCheck.CheckEquivalence(f1, f2).Take(options.InconsistencyCount).ToList(); 42 | } 43 | 44 | /// 45 | /// Prints the list of packets handled inconsistently between the two firewalls. 46 | /// 47 | /// List of inconsistencies. 48 | public static void PrintInconsistentPackets(List inconsistencies) 49 | { 50 | Console.WriteLine("Inconsistently-handled packets:"); 51 | Console.WriteLine("-------------------------------------------------------------------------"); 52 | Console.WriteLine("| PID | Src Address | Src Port | Dest Port | Protocol | Allowed By |"); 53 | Console.WriteLine("-------------------------------------------------------------------------"); 54 | for (int i = 0; i < inconsistencies.Count; i++) 55 | { 56 | WindowsFirewallInconsistency inconsistency = inconsistencies[i]; 57 | Console.WriteLine( 58 | $"| {i, 4} " + 59 | $"| {inconsistency.Packet.SourceAddress?.ToString() ?? "Any", 15} " + 60 | $"| {inconsistency.Packet.SourcePort?.ToString() ?? "Any", 8} " + 61 | $"| {inconsistency.Packet.DestinationPort?.ToString() ?? "Any", 9} " + 62 | $"| {(null == inconsistency.Packet.Protocol ? "Any" : NetworkProtocol.GetProtocolName((int)inconsistency.Packet.Protocol)), 8} " + 63 | $"| {(inconsistency.Allowed.Item1 ? "First" : "Second"), 10} |"); 64 | } 65 | 66 | Console.WriteLine("-------------------------------------------------------------------------"); 67 | } 68 | 69 | /// 70 | /// Prints list of rules matched by inconsistent packets. 71 | /// 72 | /// List of inconsistencies. 73 | public static void PrintRuleMatches(List inconsistencies) 74 | { 75 | Console.WriteLine("Firewall rules matching inconsistently-handled packets:"); 76 | Console.WriteLine("-------------------------------------------------------------------------"); 77 | Console.WriteLine("| PID | Firewall | Action | Rule Name |"); 78 | Console.WriteLine("-------------------------------------------------------------------------"); 79 | for (int i = 0; i < inconsistencies.Count; i++) 80 | { 81 | WindowsFirewallInconsistency inconsistency = inconsistencies[i]; 82 | foreach (WindowsFirewallRule rule in inconsistency.RuleMatches.Item1) 83 | { 84 | Console.WriteLine( 85 | $"| {i, 4} " + 86 | $"| {"First", 8} " + 87 | $"| {(rule.Allow ? "Allow" : "Block"), 6} " + 88 | $"| {rule.Name.Substring(0, Math.Min(rule.Name.Length, 42)), -42} |"); 89 | } 90 | 91 | foreach (WindowsFirewallRule rule in inconsistency.RuleMatches.Item2) 92 | { 93 | Console.WriteLine( 94 | $"| {i, 4} " + 95 | $"| {"Second", 8} " + 96 | $"| {(rule.Allow ? "Allow" : "Block"), 6} " + 97 | $"| {rule.Name.Substring(0, Math.Min(rule.Name.Length, 42)), -42} |"); 98 | } 99 | } 100 | 101 | Console.WriteLine("-------------------------------------------------------------------------"); 102 | } 103 | 104 | /// 105 | /// Main entry point to checker command line tool. 106 | /// 107 | /// The command line arguments. 108 | private static void Main(string[] args) 109 | { 110 | ParserResult result = Parser.Default.ParseArguments(args); 111 | if (ParserResultType.NotParsed == result.Tag) 112 | { 113 | return; 114 | } 115 | 116 | Parsed success = (Parsed)result; 117 | Options options = success.Value; 118 | List inconsistencies = Program.CheckFirewalls(options); 119 | 120 | if (!inconsistencies.Any()) 121 | { 122 | Console.ForegroundColor = ConsoleColor.Green; 123 | Console.WriteLine("Firewalls are equivalent."); 124 | Console.ResetColor(); 125 | } 126 | else 127 | { 128 | Console.ForegroundColor = ConsoleColor.Red; 129 | Console.WriteLine("Firewalls are NOT equivalent."); 130 | Console.ResetColor(); 131 | Console.WriteLine(); 132 | Program.PrintInconsistentPackets(inconsistencies); 133 | Console.WriteLine(); 134 | Program.PrintRuleMatches(inconsistencies); 135 | } 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /FirewallQueryCmd/FirewallQueryCmd.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | Exe 13 | netcoreapp5.0 14 | x64 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /FirewallQueryCmd/Options.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.FirewallQueryCmd 6 | { 7 | using CommandLine; 8 | 9 | /// 10 | /// Command line options handler. 11 | /// 12 | internal class Options 13 | { 14 | /// 15 | /// Gets or sets path to firewall rule file. 16 | /// 17 | [Option("firewall", Required = true, HelpText = "Path to tab-separated firewall rule file.")] 18 | public string FirewallFilepath { get; set; } 19 | 20 | /// 21 | /// Gets or sets a value indicating whether firewall blocks packets by default. 22 | /// 23 | [Option("blockByDefault", Required = false, Default = true, HelpText = "Whether firewall blocks packets by default.")] 24 | public bool FirewallBlockByDefault { get; set; } 25 | 26 | /// 27 | /// Gets or sets the source address of the test packet. 28 | /// 29 | [Option("srcAddress", Required = true, HelpText = "Source IP address of test packet. [127.0.0.1, \"Any\", etc.]")] 30 | public string SourceAddress { get; set; } 31 | 32 | /// 33 | /// Gets or sets the source port of the test packet. 34 | /// 35 | [Option("srcPort", Required = true, HelpText = "Source port of test packet. [80, 8080, \"Any\", etc.]")] 36 | public string SourcePort { get; set; } 37 | 38 | /// 39 | /// Gets or sets the destination port of the test packet. 40 | /// 41 | [Option("dstPort", Required = true, HelpText = "Destination port of test packet. [80, 8080, \"Any\", etc.]")] 42 | public string DestinationPort { get; set; } 43 | 44 | /// 45 | /// Gets or sets the network protocol of the test packet. 46 | /// 47 | [Option("protocol", Required = true, HelpText = "Network protocol of test packet. [TCP, UDP, 23, \"Any\", etc.]")] 48 | public string Protocol { get; set; } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /FirewallQueryCmd/Program.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | // 4 | 5 | namespace Microsoft.FirewallQueryCmd 6 | { 7 | using System; 8 | using System.Collections.Generic; 9 | using System.IO; 10 | using System.Linq; 11 | using System.Net; 12 | using CommandLine; 13 | using Microsoft.FirewallAnalysis; 14 | using Microsoft.Z3; 15 | 16 | /// 17 | /// Main entry point to query command line tool. 18 | /// 19 | internal class Program 20 | { 21 | public static void TestPacket(Options options) 22 | { 23 | Console.WriteLine("Parsing firewall rules..."); 24 | WindowsFirewall firewall = new WindowsFirewall 25 | { 26 | BlockByDefault = options.FirewallBlockByDefault, 27 | Rules = WindowsFirewallRuleParser.Parse(File.ReadAllText(options.FirewallFilepath), '\t').ToList() 28 | }; 29 | 30 | int protocolNumber; 31 | bool anyProtocol = false; 32 | if (!NetworkProtocol.TryGetProtocolNumber(options.Protocol, out protocolNumber)) 33 | { 34 | if (string.Equals("Any", options.Protocol, StringComparison.InvariantCultureIgnoreCase)) 35 | { 36 | anyProtocol = true; 37 | } 38 | else 39 | { 40 | protocolNumber = int.Parse(options.Protocol); 41 | } 42 | } 43 | 44 | WindowsFirewallPacket packet = new WindowsFirewallPacket 45 | { 46 | SourceAddress = IPAddress.Parse(options.SourceAddress), 47 | SourcePort = string.Equals("Any", options.SourcePort, StringComparison.InvariantCultureIgnoreCase) ? null : (int?)int.Parse(options.SourcePort), 48 | DestinationPort = string.Equals("Any", options.DestinationPort, StringComparison.InvariantCultureIgnoreCase) ? null : (int?)int.Parse(options.DestinationPort), 49 | Protocol = anyProtocol ? null : (int?)protocolNumber 50 | }; 51 | 52 | Console.WriteLine("Checking action of firewall on packet..."); 53 | using (var ctx = new Context()) 54 | { 55 | Solver s = ctx.MkSolver(); 56 | var packetVars = new WindowsFirewallPacketVariables(ctx); 57 | s.Assert(packet.Matches(ctx, packetVars)); 58 | s.Check(); 59 | 60 | if (s.Model.Eval(firewall.Allows(ctx, packetVars)).IsTrue) 61 | { 62 | Console.ForegroundColor = ConsoleColor.Green; 63 | Console.WriteLine("Packet is allowed by firewall."); 64 | Console.ResetColor(); 65 | } 66 | else 67 | { 68 | Console.ForegroundColor = ConsoleColor.Red; 69 | Console.WriteLine("Packet is NOT allowed by firewall."); 70 | Console.ResetColor(); 71 | } 72 | 73 | List ruleMatches = firewall.GetMatches(ctx, packetVars, s.Model).ToList(); 74 | if (!ruleMatches.Any()) 75 | { 76 | Console.WriteLine("No firewall rules match the test packet."); 77 | return; 78 | } 79 | 80 | Console.WriteLine(); 81 | Console.WriteLine("Firewall rules matching the test packet:"); 82 | Console.WriteLine("-------------------------------------------------------------------------"); 83 | Console.WriteLine("| Action | Rule Name |"); 84 | Console.WriteLine("-------------------------------------------------------------------------"); 85 | foreach (WindowsFirewallRule rule in ruleMatches) 86 | { 87 | Console.WriteLine( 88 | $"| {(rule.Allow ? "Allow" : "Block"), 6} " + 89 | $"| {rule.Name.Substring(0, Math.Min(rule.Name.Length, 60)), -60} |"); 90 | } 91 | 92 | Console.WriteLine("-------------------------------------------------------------------------"); 93 | } 94 | } 95 | 96 | /// 97 | /// Main entry point to query command line tool. 98 | /// 99 | /// The command line arguments. 100 | private static void Main(string[] args) 101 | { 102 | ParserResult result = Parser.Default.ParseArguments(args); 103 | if (ParserResultType.NotParsed == result.Tag) 104 | { 105 | return; 106 | } 107 | 108 | Parsed success = (Parsed)result; 109 | Options options = success.Value; 110 | Program.TestPacket(options); 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Z3 Theorem Prover 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FirewallChecker 2 | 3 | ![Build & test](https://github.com/z3prover/firewallchecker/workflows/build-and-test/badge.svg) 4 | 5 | A firewall analysis library using the Z3 SMT Solver from Microsoft Research. 6 | Includes console applications to check the equivalence of two firewalls, or analyze the action of a firewall on a single packet. 7 | It was developed for use inside Microsoft Azure to analyze changes to Windows Firewall generation logic. 8 | 9 | The underlying principles of operation are explained in the blog post [Checking Firewall Equivalence with Z3](https://ahelwer.ca/post/2018-02-13-z3-firewall/), and are based on the whitepaper [Checking Cloud Contracts in Microsoft Azure](https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/nbjorner-icdcit2015.pdf) by Nikolaj Bjørner and Karthick Jayaraman. 10 | 11 | ## Build & Test 12 | 13 | 1. Install the [.NET 5.0 SDK](https://dotnet.microsoft.com/download) 14 | 2. Clone the repo 15 | 3. Open your command prompt and `cd` to the repo root 16 | 4. To build, run `dotnet build` 17 | 5. To test, run `dotnet test` 18 | 19 | ## Usage 20 | 21 | This project includes two simple console applications, one which checks the equivalence of two firewalls and another which analyzes the action of a firewall on a single packet. 22 | In addition to their standalone utility, they also server as simple examples for writing more advanced applications against the Firewall Analysis library. 23 | 24 | ### Firewall Input Format 25 | 26 | The FirewallChecker consumes files of tab-separated inbound host firewall rules like those exported from Windows Firewall. To export the firewall rules of a given computer, do the following: 27 | 28 | 1. Open the Windows Run prompt with `win+r` 29 | 2. Run `mmc` (the Microsoft Management Console) 30 | 3. Open the "Add or Remove Snap-ins" prompt with `ctrl+m` 31 | 4. Add the "Windows Defender Firewall with Advanced Security" snap-in 32 | 5. Click "Inbound Rules" 33 | 6. Right-click "Inbound Rules" and click "Export List..." 34 | 7. Save the tab-separated output file 35 | 36 | The resulting file can be fed into the FirewallChecker, either for standalone analysis or comparison with a different firewall file. 37 | 38 | ### Firewall Equivalence Checker 39 | 40 | The Firewall Equivalence Checker compares two firewall rule files for logical equivalence, meaning they block or allow the same set of packets. 41 | If the two firewalls are not equivalent, the tool outputs a list of packets (default 10) treated differently by each firewall. 42 | The tool also lists firewall rules acting on the packets, easing debugging. Only IPv4 rules are currently supported. 43 | 44 | Two minimal tab-separated example firewall rule files are as follows (see [Examples](./Examples) directory): 45 | 46 | Firewall 1: 47 | ``` 48 | Name Enabled Action Local Port Remote Address Remote Port Protocol 49 | Foo1 Yes Allow 100 10.3.141.0 100 UDP 50 | Bar1 Yes Allow 200 10.3.141.0 200 TCP 51 | ``` 52 | 53 | Firewall 2: 54 | ``` 55 | Name Enabled Action Local Port Remote Address Remote Port Protocol 56 | Foo2 Yes Allow 100 10.3.141.0 100 UDP 57 | Bar2 Yes Allow 200 10.3.141.1 200 TCP 58 | ``` 59 | 60 | Analyze their differences with the following command: 61 | ``` 62 | dotnet run --project FirewallEquivalenceCheckerCmd --firewall1 Examples/firewall1.txt --firewall2 Examples/firewall2.txt 63 | ``` 64 | This should generate the following output: 65 | ``` 66 | Parsing first firewall... 67 | Parsing second firewall... 68 | Running equivalence check... 69 | Firewalls are NOT equivalent. 70 | 71 | Inconsistently-handled packets: 72 | ------------------------------------------------------------------------- 73 | | PID | Src Address | Src Port | Dest Port | Protocol | Allowed By | 74 | ------------------------------------------------------------------------- 75 | | 0 | 10.3.141.0 | 200 | 200 | TCP | First | 76 | | 1 | 10.3.141.1 | 200 | 200 | TCP | Second | 77 | ------------------------------------------------------------------------- 78 | 79 | Firewall rules matching inconsistently-handled packets: 80 | ------------------------------------------------------------------------- 81 | | PID | Firewall | Action | Rule Name | 82 | ------------------------------------------------------------------------- 83 | | 0 | First | Allow | Bar1 | 84 | | 1 | Second | Allow | Bar2 | 85 | ------------------------------------------------------------------------- 86 | ``` 87 | 88 | In the above, we have two packets with packet ID (PID) 0 and 1 which are treated differently by each firewall. 89 | The PID serves as a foreign key for the second table, which lists the rules (possibly multiple) applying to each inconsistently-handled packet. 90 | 91 | 92 | When parsing Windows Firewall rule files, there are various elements such as port macros (ex. "RPC Dynamic Ports") which the parser does not handle. 93 | These rules are simply ignored, with a warning message printed to the console for each line. 94 | 95 | Run `dotnet run --project FirewallEquivalenceCheckerCmd --help` to see the documented list of command-line parameters. 96 | 97 | ### Firewall Query 98 | 99 | The Firewall Query tool analyzes a single firewall. 100 | The tool takes as input a firewall rule file and the description of a single packet. 101 | The tool then outputs whether the firewall blocks or allows that packet, as well as a list of all firewall rules acting on that packet. 102 | Using Firewall 1 from above, we can execute a simple example query: 103 | 104 | ``` 105 | dotnet run --project FirewallQueryCmd --firewall Examples/firewall1.txt --srcAddress 10.3.141.0 --srcPort 100 --dstPort 100 --protocol UDP 106 | ``` 107 | which should produce the following output: 108 | ``` 109 | Parsing firewall rules... 110 | Checking action of firewall on packet... 111 | Packet is allowed by firewall. 112 | 113 | Firewall rules matching the test packet: 114 | ------------------------------------------------------------------------- 115 | | Action | Rule Name | 116 | ------------------------------------------------------------------------- 117 | | Allow | Foo1 | 118 | ------------------------------------------------------------------------- 119 | ``` 120 | 121 | Note that there is no `--dstAddress` parameter, as the application only works for local inbound Windows Firewall rulesets and so the destination address is assumed to be the computer on which the firewall rules live. 122 | 123 | Run `dotnet run --project FirewallQueryCmd --help` to see the documented list of command-line parameters. 124 | --------------------------------------------------------------------------------