Skip to content

Commit 68d54e3

Browse files
committed
#34: Add ability to filter events based on Data elements other than the one that contains the IP address
1 parent 7ef1f9d commit 68d54e3

File tree

8 files changed

+85
-51
lines changed

8 files changed

+85
-51
lines changed

Fail2Ban4Win/Config/Configuration.cs

+4-2
Original file line numberDiff line numberDiff line change
@@ -49,17 +49,19 @@ public class EventLogSelector: ICloneable {
4949
public Regex? ipAddressPattern { get; set; }
5050
public string? ipAddressEventDataName { get; set; }
5151
public int ipAddressEventDataIndex { get; set; }
52+
public string? eventPredicate { get; set; }
5253

5354
public override string ToString() =>
54-
$"{nameof(logName)}: {logName}, {nameof(source)}: {source}, {nameof(eventId)}: {eventId}, {nameof(ipAddressPattern)}: {ipAddressPattern}, {nameof(ipAddressEventDataName)}: {ipAddressEventDataName}, {nameof(ipAddressEventDataIndex)}: {ipAddressEventDataIndex}";
55+
$"{nameof(logName)}: {logName}, {nameof(source)}: {source}, {nameof(eventId)}: {eventId}, {nameof(ipAddressPattern)}: {ipAddressPattern}, {nameof(ipAddressEventDataName)}: {ipAddressEventDataName}, {nameof(ipAddressEventDataIndex)}: {ipAddressEventDataIndex}, {nameof(eventPredicate)}: {eventPredicate}";
5556

5657
public object Clone() => new EventLogSelector {
5758
ipAddressEventDataName = ipAddressEventDataName,
5859
eventId = eventId,
5960
ipAddressPattern = ipAddressPattern is not null ? new Regex(ipAddressPattern.ToString(), ipAddressPattern.Options, ipAddressPattern.MatchTimeout) : null,
6061
logName = logName,
6162
source = source,
62-
ipAddressEventDataIndex = ipAddressEventDataIndex
63+
ipAddressEventDataIndex = ipAddressEventDataIndex,
64+
eventPredicate = eventPredicate
6365
};
6466

6567
}

Fail2Ban4Win/Properties/AssemblyInfo.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using System.Reflection;
1+
using System.Reflection;
22
using System.Runtime.CompilerServices;
33
using System.Runtime.InteropServices;
44

@@ -32,7 +32,7 @@
3232
// You can specify all the values or you can default the Build and Revision Numbers
3333
// by using the '*' as shown below:
3434
// [assembly: AssemblyVersion("1.0.*")]
35-
[assembly: AssemblyVersion("1.2.1.0")]
36-
[assembly: AssemblyFileVersion("1.2.1.0")]
35+
[assembly: AssemblyVersion("1.3.0.0")]
36+
[assembly: AssemblyFileVersion("1.3.0.0")]
3737

3838
[assembly: InternalsVisibleTo("Tests")]

Fail2Ban4Win/Services/EventLogListener.cs

+5-1
Original file line numberDiff line numberDiff line change
@@ -100,10 +100,14 @@ private static string selectorToQuery(EventLogSelector selector) {
100100
StringBuilder queryBuilder = new("*");
101101
queryBuilder.Append($"[System/EventID={selector.eventId}]");
102102

103-
if (selector.source is not null) {
103+
if (!string.IsNullOrWhiteSpace(selector.source)) {
104104
queryBuilder.Append($"[System/Provider/@Name=\"{SecurityElement.Escape(selector.source)}\"]");
105105
}
106106

107+
if (!string.IsNullOrWhiteSpace(selector.eventPredicate)) {
108+
queryBuilder.Append(selector.eventPredicate);
109+
}
110+
107111
return queryBuilder.ToString();
108112
}
109113

Readme.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<img src="https://raw.githubusercontent.com/Aldaviva/Fail2Ban4Win/master/Fail2Ban4Win/pifmgr_37.ico" height="32" alt="Fail2Ban4Win logo" /> Fail2Ban4Win
1+
<img src="https://raw.githubusercontent.com/Aldaviva/Fail2Ban4Win/master/Fail2Ban4Win/pifmgr_37.ico" height="32" alt="Fail2Ban4Win logo" /> Fail2Ban4Win
22
===
33

44
![price: free](https://img.shields.io/badge/price-free-brightgreen) [![Build status](https://img.shields.io/github/actions/workflow/status/Aldaviva/Fail2Ban4Win/dotnetframework.yml?branch=master&logo=github)](https://github.com/Aldaviva/Fail2Ban4Win/actions/workflows/dotnetframework.yml) [![Test status](https://img.shields.io/testspace/tests/Aldaviva/Aldaviva:Fail2Ban4Win/master?passed_label=passing&failed_label=failing&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA4NTkgODYxIj48cGF0aCBkPSJtNTk4IDUxMy05NCA5NCAyOCAyNyA5NC05NC0yOC0yN3pNMzA2IDIyNmwtOTQgOTQgMjggMjggOTQtOTQtMjgtMjh6bS00NiAyODctMjcgMjcgOTQgOTQgMjctMjctOTQtOTR6bTI5My0yODctMjcgMjggOTQgOTQgMjctMjgtOTQtOTR6TTQzMiA4NjFjNDEuMzMgMCA3Ni44My0xNC42NyAxMDYuNS00NFM1ODMgNzUyIDU4MyA3MTBjMC00MS4zMy0xNC44My03Ni44My00NC41LTEwNi41UzQ3My4zMyA1NTkgNDMyIDU1OWMtNDIgMC03Ny42NyAxNC44My0xMDcgNDQuNXMtNDQgNjUuMTctNDQgMTA2LjVjMCA0MiAxNC42NyA3Ny42NyA0NCAxMDdzNjUgNDQgMTA3IDQ0em0wLTU1OWM0MS4zMyAwIDc2LjgzLTE0LjgzIDEwNi41LTQ0LjVTNTgzIDE5Mi4zMyA1ODMgMTUxYzAtNDItMTQuODMtNzcuNjctNDQuNS0xMDdTNDczLjMzIDAgNDMyIDBjLTQyIDAtNzcuNjcgMTQuNjctMTA3IDQ0cy00NCA2NS00NCAxMDdjMCA0MS4zMyAxNC42NyA3Ni44MyA0NCAxMDYuNVMzOTAgMzAyIDQzMiAzMDJ6bTI3NiAyODJjNDIgMCA3Ny42Ny0xNC44MyAxMDctNDQuNXM0NC02NS4xNyA0NC0xMDYuNWMwLTQyLTE0LjY3LTc3LjY3LTQ0LTEwN3MtNjUtNDQtMTA3LTQ0Yy00MS4zMyAwLTc2LjY3IDE0LjY3LTEwNiA0NHMtNDQgNjUtNDQgMTA3YzAgNDEuMzMgMTQuNjcgNzYuODMgNDQgMTA2LjVTNjY2LjY3IDU4NCA3MDggNTg0em0tNTU3IDBjNDIgMCA3Ny42Ny0xNC44MyAxMDctNDQuNXM0NC02NS4xNyA0NC0xMDYuNWMwLTQyLTE0LjY3LTc3LjY3LTQ0LTEwN3MtNjUtNDQtMTA3LTQ0Yy00MS4zMyAwLTc2LjgzIDE0LjY3LTEwNi41IDQ0UzAgMzkxIDAgNDMzYzAgNDEuMzMgMTQuODMgNzYuODMgNDQuNSAxMDYuNVMxMDkuNjcgNTg0IDE1MSA1ODR6IiBmaWxsPSIjZmZmIi8%2BPC9zdmc%2B)](https://aldaviva.testspace.com/spaces/194263) [![Coverage status](https://img.shields.io/coveralls/github/Aldaviva/Fail2Ban4Win?logo=coveralls)](https://coveralls.io/github/Aldaviva/Fail2Ban4Win?branch=master)
@@ -86,7 +86,7 @@ Be aware that `isDryRun` defaults to `true` to avoid accidentally blocking traff
8686
|`logLevel`|`Info`|Optionally adjust the logging verbosity of Fail2Ban4Win. Valid values are `Trace` (most verbose), `Debug`, `Info`, `Warn`, `Error`, and `Fatal` (least verbose). All messages at the given level will be logged, as well as all messages at less verbose levels, _i.e._ `Warn` will also log `Error` and `Fatal` messages. To see the log output, you must run `Fail2Ban4Win.exe` in a console like Command Prompt or PowerShell.|
8787
|`neverBanSubnets`|`[]`|Optional whitelist of IP ranges that should never be banned, regardless of how many auth failures they generate. Each item can be a single IP address, like `67.210.32.33`, or a range, like `67.210.32.0/24`.|
8888
|`neverBanReservedSubnets`|`true`|By default, IP addresses in the reserved blocks `10.0.0.0/8`, `172.16.0.0/12`, and `192.168.0.0/16` will not be banned, to avoid unintentionally blocking LAN access. To allow all three ranges to be banned, change this to `false`. To then selectively prevent some of those ranges from getting banned, you may add them to the `neverBanSubnets` list above. Regardless of this configuration, the loopback address will never be banned.|
89-
|`eventLogSelectors`|`[]`|Required list of events to listen for in Event Log. Each object in the list can have the following properties.<ul><li>`logName`: required, which log in Event Viewer contains the events, _e.g._ `Application`, `Security`, `OpenSSH/Operational`.</li><li>`eventId`: required, numeric ID of event logged on auth failure, _e.g._ `4625` for RDP auth errors.</li><li>`source`: optional Source, AKA Provider Name, of events, _e.g._ `sshd` for Cygwin OpenSSH sshd. If omitted, events will not be filtered by Source.</li><li>`ipAddressEventDataName`: optional, the `Name` of the `Data` element in the event XML's `EventData` in which to search for the client IP address of the auth request, _e.g._ `IpAddress` for RDP. If omitted, the first `Data` element will be searched instead.</li><li>`ipAddressEventDataIndex`: optional, the 0-indexed offset of the `Data` element in the XML's `EventData` in which to search for the client IP address, _e.g._ `3` to search for IP addresses in the fourth `Data` element in `EventData`. Useful if `EventData` has multiple `Data` children, but none of them have a `Name` attribute to specify in `ipAddressEventDataName`, and the IP address doesn't appear in the first one. This offset is applied after any `Name` attribute filtering, and applies whether or not `ipAddressEventDataName` is specified. If omitted, defaults to `0`.</li><li>`ipAddressPattern`: optional, regular expression pattern string that matches the IP address in the `Data` element specified above. Useful if you want to filter out some events from the log with the desired ID and source but that don't describe an auth failure (_e.g._ sshd's disconnect events). If omitted, searches for all IPv4 addresses in the `Data` element's text content. To set [options like case-insensitivity](https://docs.microsoft.com/en-us/dotnet/standard/base-types/miscellaneous-constructs-in-regular-expressions), put `(?i)` at the start of the pattern. Patterns are not anchored to the entire input string unless you surround them with `^` and `$`. If you specify a pattern, ensure the desired IPv4 capture group in your pattern has the name `ipAddress`, _e.g._ <pre lang="regex">Failed: (?&lt;ipAddress&gt;(?:\d{1,3}\\.){3}\d{1,3})</pre></li></ul>See [Handling a new event](#handling-a-new-event) below for a tutorial on creating this object.|
89+
|`eventLogSelectors`|`[]`|Required list of events to listen for in Event Log. Each object in the list can have the following properties.<ul><li>`logName`: required, which log in Event Viewer contains the events, _e.g._ `Application`, `Security`, `OpenSSH/Operational`.</li><li>`eventId`: required, numeric ID of event logged on auth failure, _e.g._ `4625` for RDP auth errors.</li><li>`source`: optional Source, AKA Provider Name, of events, _e.g._ `sshd` for Cygwin OpenSSH sshd. If omitted, events will not be filtered by Source.</li><li>`ipAddressEventDataName`: optional, the `Name` of the `Data` element in the event XML's `EventData` in which to search for the client IP address of the auth request, _e.g._ `IpAddress` for RDP. If omitted, the first `Data` element will be searched instead.</li><li>`ipAddressEventDataIndex`: optional, the 0-indexed offset of the `Data` element in the XML's `EventData` in which to search for the client IP address, _e.g._ `3` to search for IP addresses in the fourth `Data` element in `EventData`. Useful if `EventData` has multiple `Data` children, but none of them have a `Name` attribute to specify in `ipAddressEventDataName`, and the IP address doesn't appear in the first one. This offset is applied after any `Name` attribute filtering, and applies whether or not `ipAddressEventDataName` is specified. If omitted, defaults to `0`.</li><li>`ipAddressPattern`: optional, regular expression pattern string that matches the IP address in the `Data` element specified above. Useful if you want to filter out some events from the log with the desired ID and source but that don't describe an auth failure (_e.g._ sshd's disconnect events). If omitted, searches for all IPv4 addresses in the `Data` element's text content. To set [options like case-insensitivity](https://docs.microsoft.com/en-us/dotnet/standard/base-types/miscellaneous-constructs-in-regular-expressions), put `(?i)` at the start of the pattern. Patterns are not anchored to the entire input string unless you surround them with `^` and `$`. If you specify a pattern, ensure the desired IPv4 capture group in your pattern has the name `ipAddress`, _e.g._ <pre lang="regex">Failed: (?&lt;ipAddress&gt;(?:\d{1,3}\\.){3}\d{1,3})</pre></li><li>`eventPredicate`: optional, XPath 1.0 query fragment to filter events based on arbitrary elements, matched against the `<Event>` element. Useful if not all events with the given `logName`, `eventId`, and `source` should trigger bans, like IIS HTTP 200 responses, _e.g._ `[EventData/Data[@Name='sc-status']=403]`. Most XPath functions are not supported by Windows ETW.</li></ul>See [Handling a new event](#handling-a-new-event) below for a tutorial on creating this object.|
9090
1. After saving the configuration file, restart the Fail2Ban4Win service using `services.msc` (GUI), `Restart-Service Fail2Ban4Win` (PowerShell), or `net stop Fail2Ban4Win & net start Fail2Ban4Win` (Command Prompt) for your changes to take effect. Note that the service will clear existing bans when it starts.
9191
9292
<a id="handling-a-new-event"></a>

Tests/Config/ConfigurationTest.cs

+15-2
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,13 @@ public ConfigurationTest(ITestOutputHelper testOutputHelper) {
5555
"logName": "Application",
5656
"source": "MSExchangeFrontEndTransport",
5757
"eventId": 1035,
58-
"ipAddressEventDataIndex": 3
58+
"ipAddressEventDataIndex": 3,
59+
}, {
60+
"logName": "Microsoft-Windows-IIS-Logging/Logs",
61+
"source": "IIS-Logging",
62+
"eventId": 6200,
63+
"ipAddressEventDataName": "c-ip",
64+
"eventPredicate": "[EventData/Data[@Name='sc-status']=403]"
5965
}
6066
],
6167
"isDryRun": true,
@@ -90,7 +96,7 @@ public void parse() {
9096
Assert.NotNull(actual.ToString());
9197

9298
EventLogSelector[] actualSelectors = actual.eventLogSelectors.ToArray();
93-
Assert.Equal(3, actualSelectors.Length);
99+
Assert.Equal(4, actualSelectors.Length);
94100

95101
EventLogSelector rdpSelector = actualSelectors[0];
96102
Assert.Equal("Security", rdpSelector.logName);
@@ -119,6 +125,13 @@ public void parse() {
119125
Assert.Equal("MSExchangeFrontEndTransport", exchangeFrontendSelector.source);
120126
Assert.Equal(3, exchangeFrontendSelector.ipAddressEventDataIndex);
121127
Assert.NotNull(exchangeFrontendSelector.ToString());
128+
129+
EventLogSelector iisSelector = actualSelectors[3];
130+
Assert.Equal("Microsoft-Windows-IIS-Logging/Logs", iisSelector.logName);
131+
Assert.Equal("IIS-Logging", iisSelector.source);
132+
Assert.Equal(6200, iisSelector.eventId);
133+
Assert.Equal("c-ip", iisSelector.ipAddressEventDataName);
134+
Assert.Equal("[EventData/Data[@Name='sc-status']=403]", iisSelector.eventPredicate);
122135
}
123136

124137
public void Dispose() {

Tests/Data/EnumerableExtensionsTest.cs

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
#nullable enable
22

3-
using System.Collections.Generic;
43
using Fail2Ban4Win.Data;
4+
using System.Collections.Generic;
55
using Xunit;
66

7-
namespace Tests.Data;
7+
namespace Tests.Data;
88

99
public class EnumerableExtensionsTest {
1010

1111
[Fact]
1212
public void classes() {
13-
IEnumerable<string?> input = new[] { "hello", null, "world" };
13+
IEnumerable<string?> input = ["hello", null, "world"];
1414
IEnumerable<string> actual = input.Compact();
15-
IEnumerable<string> expected = new[] { "hello", "world" };
15+
IEnumerable<string> expected = ["hello", "world"];
1616

1717
Assert.Equal(expected, actual);
1818
}

Tests/Services/BanManagerTest.cs

+22-22
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public class BanManagerTest: IDisposable {
3939
banRepeatedOffenseMax = 4,
4040
logLevel = LogLevel.Trace,
4141
maxAllowedFailures = MAX_ALLOWED_FAILURES,
42-
neverBanSubnets = new[] { IPNetwork2.Parse("73.202.12.148/32") }
42+
neverBanSubnets = [IPNetwork2.Parse("73.202.12.148/32")]
4343
};
4444

4545
private readonly FakeFirewallRulesCollection firewallRules = new();
@@ -191,10 +191,10 @@ public void deleteExistingRulesOnStartup() {
191191

192192
[Fact]
193193
public void unbanAfterBanExpired() {
194-
ICollection<IPAddress> sourceAddresses = new[] {
194+
ICollection<IPAddress> sourceAddresses = [
195195
IPAddress.Parse("198.51.100.1"),
196196
IPAddress.Parse("101.206.243.0")
197-
};
197+
];
198198

199199
CountdownEvent rulesRemoved = new(sourceAddresses.Count);
200200
firewallRules.ruleRemoved += (_, _) => rulesRemoved.Signal();
@@ -248,26 +248,26 @@ public void banDuration(int offense, double coefficient, TimeSpan expectedDurati
248248
Assert.Equal(expectedDuration, actual);
249249
}
250250

251-
public static readonly IEnumerable<object[]> BAN_DURATION_DATA = new[] {
251+
public static readonly IEnumerable<object[]> BAN_DURATION_DATA = [
252252
new object[] { 1, 1.0, TimeSpan.FromMinutes(1) },
253-
new object[] { 2, 1.0, TimeSpan.FromMinutes(2) },
254-
new object[] { 3, 1.0, TimeSpan.FromMinutes(3) },
255-
new object[] { 4, 1.0, TimeSpan.FromMinutes(4) },
256-
new object[] { 5, 1.0, TimeSpan.FromMinutes(4) },
257-
new object[] { 6, 1.0, TimeSpan.FromMinutes(4) },
258-
new object[] { 1, 1.5, TimeSpan.FromMinutes(1) },
259-
new object[] { 2, 1.5, TimeSpan.FromMinutes(2.5) },
260-
new object[] { 3, 1.5, TimeSpan.FromMinutes(4) },
261-
new object[] { 4, 1.5, TimeSpan.FromMinutes(5.5) },
262-
new object[] { 5, 1.5, TimeSpan.FromMinutes(5.5) },
263-
new object[] { 6, 1.5, TimeSpan.FromMinutes(5.5) },
264-
new object[] { 1, 2.0, TimeSpan.FromMinutes(1) },
265-
new object[] { 2, 2.0, TimeSpan.FromMinutes(3) },
266-
new object[] { 3, 2.0, TimeSpan.FromMinutes(5) },
267-
new object[] { 4, 2.0, TimeSpan.FromMinutes(7) },
268-
new object[] { 5, 2.0, TimeSpan.FromMinutes(7) },
269-
new object[] { 6, 2.0, TimeSpan.FromMinutes(7) },
270-
};
253+
[2, 1.0, TimeSpan.FromMinutes(2)],
254+
[3, 1.0, TimeSpan.FromMinutes(3)],
255+
[4, 1.0, TimeSpan.FromMinutes(4)],
256+
[5, 1.0, TimeSpan.FromMinutes(4)],
257+
[6, 1.0, TimeSpan.FromMinutes(4)],
258+
[1, 1.5, TimeSpan.FromMinutes(1)],
259+
[2, 1.5, TimeSpan.FromMinutes(2.5)],
260+
[3, 1.5, TimeSpan.FromMinutes(4)],
261+
[4, 1.5, TimeSpan.FromMinutes(5.5)],
262+
[5, 1.5, TimeSpan.FromMinutes(5.5)],
263+
[6, 1.5, TimeSpan.FromMinutes(5.5)],
264+
[1, 2.0, TimeSpan.FromMinutes(1)],
265+
[2, 2.0, TimeSpan.FromMinutes(3)],
266+
[3, 2.0, TimeSpan.FromMinutes(5)],
267+
[4, 2.0, TimeSpan.FromMinutes(7)],
268+
[5, 2.0, TimeSpan.FromMinutes(7)],
269+
[6, 2.0, TimeSpan.FromMinutes(7)],
270+
];
271271

272272
[Fact]
273273
public void longDelaysDoNotCrash() {

0 commit comments

Comments
 (0)