├── .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 | 
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 |
--------------------------------------------------------------------------------