diff --git a/Fail2Ban4Win/Config/Configuration.cs b/Fail2Ban4Win/Config/Configuration.cs
index 58c51e1..0ed6b8c 100644
--- a/Fail2Ban4Win/Config/Configuration.cs
+++ b/Fail2Ban4Win/Config/Configuration.cs
@@ -49,9 +49,10 @@ 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,
@@ -59,7 +60,8 @@ public override string ToString() =>
ipAddressPattern = ipAddressPattern is not null ? new Regex(ipAddressPattern.ToString(), ipAddressPattern.Options, ipAddressPattern.MatchTimeout) : null,
logName = logName,
source = source,
- ipAddressEventDataIndex = ipAddressEventDataIndex
+ ipAddressEventDataIndex = ipAddressEventDataIndex,
+ eventPredicate = eventPredicate
};
}
\ No newline at end of file
diff --git a/Fail2Ban4Win/Properties/AssemblyInfo.cs b/Fail2Ban4Win/Properties/AssemblyInfo.cs
index 6378bf8..811bb23 100644
--- a/Fail2Ban4Win/Properties/AssemblyInfo.cs
+++ b/Fail2Ban4Win/Properties/AssemblyInfo.cs
@@ -1,4 +1,4 @@
-using System.Reflection;
+using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@@ -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")]
\ No newline at end of file
diff --git a/Fail2Ban4Win/Services/EventLogListener.cs b/Fail2Ban4Win/Services/EventLogListener.cs
index 10fbf4f..e5ec9da 100644
--- a/Fail2Ban4Win/Services/EventLogListener.cs
+++ b/Fail2Ban4Win/Services/EventLogListener.cs
@@ -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();
}
diff --git a/Readme.md b/Readme.md
index ca2ec37..c84d1bf 100644
--- a/Readme.md
+++ b/Readme.md
@@ -1,4 +1,4 @@
- Fail2Ban4Win
+ 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)
@@ -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.
`logName`: required, which log in Event Viewer contains the events, _e.g._ `Application`, `Security`, `OpenSSH/Operational`.
`eventId`: required, numeric ID of event logged on auth failure, _e.g._ `4625` for RDP auth errors.
`source`: optional Source, AKA Provider Name, of events, _e.g._ `sshd` for Cygwin OpenSSH sshd. If omitted, events will not be filtered by Source.
`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.
`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`.
`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._
Failed: (?<ipAddress>(?:\d{1,3}\\.){3}\d{1,3})
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.
`logName`: required, which log in Event Viewer contains the events, _e.g._ `Application`, `Security`, `OpenSSH/Operational`.
`eventId`: required, numeric ID of event logged on auth failure, _e.g._ `4625` for RDP auth errors.
`source`: optional Source, AKA Provider Name, of events, _e.g._ `sshd` for Cygwin OpenSSH sshd. If omitted, events will not be filtered by Source.
`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.
`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`.
`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._
Failed: (?<ipAddress>(?:\d{1,3}\\.){3}\d{1,3})
`eventPredicate`: optional, XPath 1.0 query fragment to filter events based on arbitrary elements, matched against the `` 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.
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.
diff --git a/Tests/Config/ConfigurationTest.cs b/Tests/Config/ConfigurationTest.cs
index ff52ad0..935df95 100644
--- a/Tests/Config/ConfigurationTest.cs
+++ b/Tests/Config/ConfigurationTest.cs
@@ -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,
@@ -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);
@@ -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() {
diff --git a/Tests/Data/EnumerableExtensionsTest.cs b/Tests/Data/EnumerableExtensionsTest.cs
index 479cc9b..d42e99b 100644
--- a/Tests/Data/EnumerableExtensionsTest.cs
+++ b/Tests/Data/EnumerableExtensionsTest.cs
@@ -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 input = new[] { "hello", null, "world" };
+ IEnumerable input = ["hello", null, "world"];
IEnumerable actual = input.Compact();
- IEnumerable expected = new[] { "hello", "world" };
+ IEnumerable expected = ["hello", "world"];
Assert.Equal(expected, actual);
}
diff --git a/Tests/Services/BanManagerTest.cs b/Tests/Services/BanManagerTest.cs
index 79fa721..12a5747 100644
--- a/Tests/Services/BanManagerTest.cs
+++ b/Tests/Services/BanManagerTest.cs
@@ -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();
@@ -191,10 +191,10 @@ public void deleteExistingRulesOnStartup() {
[Fact]
public void unbanAfterBanExpired() {
- ICollection sourceAddresses = new[] {
+ ICollection sourceAddresses = [
IPAddress.Parse("198.51.100.1"),
IPAddress.Parse("101.206.243.0")
- };
+ ];
CountdownEvent rulesRemoved = new(sourceAddresses.Count);
firewallRules.ruleRemoved += (_, _) => rulesRemoved.Signal();
@@ -248,26 +248,26 @@ public void banDuration(int offense, double coefficient, TimeSpan expectedDurati
Assert.Equal(expectedDuration, actual);
}
- public static readonly IEnumerable