Skip to content

Commit

Permalink
#34: Add ability to filter events based on Data elements other than t…
Browse files Browse the repository at this point in the history
…he one that contains the IP address
  • Loading branch information
Aldaviva committed Aug 3, 2024
1 parent 7ef1f9d commit 68d54e3
Show file tree
Hide file tree
Showing 8 changed files with 85 additions and 51 deletions.
6 changes: 4 additions & 2 deletions Fail2Ban4Win/Config/Configuration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,17 +49,19 @@ public class EventLogSelector: ICloneable {
public Regex? ipAddressPattern { get; set; }
public string? ipAddressEventDataName { get; set; }
public int ipAddressEventDataIndex { get; set; }
public string? eventPredicate { get; set; }

public override string ToString() =>
$"{nameof(logName)}: {logName}, {nameof(source)}: {source}, {nameof(eventId)}: {eventId}, {nameof(ipAddressPattern)}: {ipAddressPattern}, {nameof(ipAddressEventDataName)}: {ipAddressEventDataName}, {nameof(ipAddressEventDataIndex)}: {ipAddressEventDataIndex}";
$"{nameof(logName)}: {logName}, {nameof(source)}: {source}, {nameof(eventId)}: {eventId}, {nameof(ipAddressPattern)}: {ipAddressPattern}, {nameof(ipAddressEventDataName)}: {ipAddressEventDataName}, {nameof(ipAddressEventDataIndex)}: {ipAddressEventDataIndex}, {nameof(eventPredicate)}: {eventPredicate}";

public object Clone() => new EventLogSelector {
ipAddressEventDataName = ipAddressEventDataName,
eventId = eventId,
ipAddressPattern = ipAddressPattern is not null ? new Regex(ipAddressPattern.ToString(), ipAddressPattern.Options, ipAddressPattern.MatchTimeout) : null,
logName = logName,
source = source,
ipAddressEventDataIndex = ipAddressEventDataIndex
ipAddressEventDataIndex = ipAddressEventDataIndex,
eventPredicate = eventPredicate
};

}
6 changes: 3 additions & 3 deletions Fail2Ban4Win/Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.Reflection;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

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

[assembly: InternalsVisibleTo("Tests")]
6 changes: 5 additions & 1 deletion Fail2Ban4Win/Services/EventLogListener.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,10 +100,14 @@ private static string selectorToQuery(EventLogSelector selector) {
StringBuilder queryBuilder = new("*");
queryBuilder.Append($"[System/EventID={selector.eventId}]");

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

if (!string.IsNullOrWhiteSpace(selector.eventPredicate)) {
queryBuilder.Append(selector.eventPredicate);
}

return queryBuilder.ToString();
}

Expand Down
4 changes: 2 additions & 2 deletions Readme.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<img src="https://raw.githubusercontent.com/Aldaviva/Fail2Ban4Win/master/Fail2Ban4Win/pifmgr_37.ico" height="32" alt="Fail2Ban4Win logo" /> Fail2Ban4Win
<img src="https://raw.githubusercontent.com/Aldaviva/Fail2Ban4Win/master/Fail2Ban4Win/pifmgr_37.ico" height="32" alt="Fail2Ban4Win logo" /> Fail2Ban4Win
===

![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)
Expand Down Expand Up @@ -86,7 +86,7 @@ Be aware that `isDryRun` defaults to `true` to avoid accidentally blocking traff
|`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.|
|`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`.|
|`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.|
|`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.|
|`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.|
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.
<a id="handling-a-new-event"></a>
Expand Down
17 changes: 15 additions & 2 deletions Tests/Config/ConfigurationTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,13 @@ public ConfigurationTest(ITestOutputHelper testOutputHelper) {
"logName": "Application",
"source": "MSExchangeFrontEndTransport",
"eventId": 1035,
"ipAddressEventDataIndex": 3
"ipAddressEventDataIndex": 3,
}, {
"logName": "Microsoft-Windows-IIS-Logging/Logs",
"source": "IIS-Logging",
"eventId": 6200,
"ipAddressEventDataName": "c-ip",
"eventPredicate": "[EventData/Data[@Name='sc-status']=403]"
}
],
"isDryRun": true,
Expand Down Expand Up @@ -90,7 +96,7 @@ public void parse() {
Assert.NotNull(actual.ToString());

EventLogSelector[] actualSelectors = actual.eventLogSelectors.ToArray();
Assert.Equal(3, actualSelectors.Length);
Assert.Equal(4, actualSelectors.Length);

EventLogSelector rdpSelector = actualSelectors[0];
Assert.Equal("Security", rdpSelector.logName);
Expand Down Expand Up @@ -119,6 +125,13 @@ public void parse() {
Assert.Equal("MSExchangeFrontEndTransport", exchangeFrontendSelector.source);
Assert.Equal(3, exchangeFrontendSelector.ipAddressEventDataIndex);
Assert.NotNull(exchangeFrontendSelector.ToString());

EventLogSelector iisSelector = actualSelectors[3];
Assert.Equal("Microsoft-Windows-IIS-Logging/Logs", iisSelector.logName);
Assert.Equal("IIS-Logging", iisSelector.source);
Assert.Equal(6200, iisSelector.eventId);
Assert.Equal("c-ip", iisSelector.ipAddressEventDataName);
Assert.Equal("[EventData/Data[@Name='sc-status']=403]", iisSelector.eventPredicate);
}

public void Dispose() {
Expand Down
8 changes: 4 additions & 4 deletions Tests/Data/EnumerableExtensionsTest.cs
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
#nullable enable

using System.Collections.Generic;
using Fail2Ban4Win.Data;
using System.Collections.Generic;
using Xunit;

namespace Tests.Data;
namespace Tests.Data;

public class EnumerableExtensionsTest {

[Fact]
public void classes() {
IEnumerable<string?> input = new[] { "hello", null, "world" };
IEnumerable<string?> input = ["hello", null, "world"];
IEnumerable<string> actual = input.Compact();
IEnumerable<string> expected = new[] { "hello", "world" };
IEnumerable<string> expected = ["hello", "world"];

Assert.Equal(expected, actual);
}
Expand Down
44 changes: 22 additions & 22 deletions Tests/Services/BanManagerTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public class BanManagerTest: IDisposable {
banRepeatedOffenseMax = 4,
logLevel = LogLevel.Trace,
maxAllowedFailures = MAX_ALLOWED_FAILURES,
neverBanSubnets = new[] { IPNetwork2.Parse("73.202.12.148/32") }
neverBanSubnets = [IPNetwork2.Parse("73.202.12.148/32")]
};

private readonly FakeFirewallRulesCollection firewallRules = new();
Expand Down Expand Up @@ -191,10 +191,10 @@ public void deleteExistingRulesOnStartup() {

[Fact]
public void unbanAfterBanExpired() {
ICollection<IPAddress> sourceAddresses = new[] {
ICollection<IPAddress> sourceAddresses = [
IPAddress.Parse("198.51.100.1"),
IPAddress.Parse("101.206.243.0")
};
];

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

public static readonly IEnumerable<object[]> BAN_DURATION_DATA = new[] {
public static readonly IEnumerable<object[]> BAN_DURATION_DATA = [
new object[] { 1, 1.0, TimeSpan.FromMinutes(1) },
new object[] { 2, 1.0, TimeSpan.FromMinutes(2) },
new object[] { 3, 1.0, TimeSpan.FromMinutes(3) },
new object[] { 4, 1.0, TimeSpan.FromMinutes(4) },
new object[] { 5, 1.0, TimeSpan.FromMinutes(4) },
new object[] { 6, 1.0, TimeSpan.FromMinutes(4) },
new object[] { 1, 1.5, TimeSpan.FromMinutes(1) },
new object[] { 2, 1.5, TimeSpan.FromMinutes(2.5) },
new object[] { 3, 1.5, TimeSpan.FromMinutes(4) },
new object[] { 4, 1.5, TimeSpan.FromMinutes(5.5) },
new object[] { 5, 1.5, TimeSpan.FromMinutes(5.5) },
new object[] { 6, 1.5, TimeSpan.FromMinutes(5.5) },
new object[] { 1, 2.0, TimeSpan.FromMinutes(1) },
new object[] { 2, 2.0, TimeSpan.FromMinutes(3) },
new object[] { 3, 2.0, TimeSpan.FromMinutes(5) },
new object[] { 4, 2.0, TimeSpan.FromMinutes(7) },
new object[] { 5, 2.0, TimeSpan.FromMinutes(7) },
new object[] { 6, 2.0, TimeSpan.FromMinutes(7) },
};
[2, 1.0, TimeSpan.FromMinutes(2)],
[3, 1.0, TimeSpan.FromMinutes(3)],
[4, 1.0, TimeSpan.FromMinutes(4)],
[5, 1.0, TimeSpan.FromMinutes(4)],
[6, 1.0, TimeSpan.FromMinutes(4)],
[1, 1.5, TimeSpan.FromMinutes(1)],
[2, 1.5, TimeSpan.FromMinutes(2.5)],
[3, 1.5, TimeSpan.FromMinutes(4)],
[4, 1.5, TimeSpan.FromMinutes(5.5)],
[5, 1.5, TimeSpan.FromMinutes(5.5)],
[6, 1.5, TimeSpan.FromMinutes(5.5)],
[1, 2.0, TimeSpan.FromMinutes(1)],
[2, 2.0, TimeSpan.FromMinutes(3)],
[3, 2.0, TimeSpan.FromMinutes(5)],
[4, 2.0, TimeSpan.FromMinutes(7)],
[5, 2.0, TimeSpan.FromMinutes(7)],
[6, 2.0, TimeSpan.FromMinutes(7)],
];

[Fact]
public void longDelaysDoNotCrash() {
Expand Down
Loading

0 comments on commit 68d54e3

Please sign in to comment.