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 logo Fail2Ban4Win +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) @@ -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.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.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 BAN_DURATION_DATA = new[] { + public static readonly IEnumerable 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() { diff --git a/Tests/Services/EventLogListenerTest.cs b/Tests/Services/EventLogListenerTest.cs index b801159..8372939 100644 --- a/Tests/Services/EventLogListenerTest.cs +++ b/Tests/Services/EventLogListenerTest.cs @@ -26,8 +26,8 @@ public class EventLogListenerTest: IDisposable { banSubnetBits = 8, logLevel = LogLevel.Trace, maxAllowedFailures = 2, - neverBanSubnets = new[] { IPNetwork2.Parse("73.202.12.148/32") }, - eventLogSelectors = new[] { + neverBanSubnets = [IPNetwork2.Parse("73.202.12.148/32")], + eventLogSelectors = [ new EventLogSelector { logName = "Security", eventId = 4625, @@ -44,14 +44,21 @@ public class EventLogListenerTest: IDisposable { source = "MSExchangeFrontEndTransport", eventId = 1035, ipAddressEventDataIndex = 3 + }, + new EventLogSelector { + logName = "Microsoft-Windows-IIS-Logging/Logs", + source = "IIS-Logging", + eventId = 6200, + ipAddressEventDataName = "c-ip", + eventPredicate = "[EventData/Data[@Name='sc-status']=403]" } - } + ] }; private readonly EventLogListener listener; - private readonly IList watcherFacades = new List(); - private readonly IList queries = new List(); + private readonly IList watcherFacades = []; + private readonly IList queries = []; public EventLogListenerTest(ITestOutputHelper testOutputHelper) { XunitTestOutputTarget.start(testOutputHelper); @@ -86,10 +93,18 @@ public void queryWithSource() { Assert.Equal("*[System/EventID=0][System/Provider/@Name=\"sshd\"]", actual.query); } + [Fact] + public void queryWithSourceAndPredicate() { + EventLogQueryFacade actual = queries[3]; + Assert.Equal("Microsoft-Windows-IIS-Logging/Logs", actual.path); + Assert.Equal(PathType.LogName, actual.pathType); + Assert.Equal("*[System/EventID=6200][System/Provider/@Name=\"IIS-Logging\"][EventData/Data[@Name='sc-status']=403]", actual.query); + } + [Fact] public void builtInPattern() { EventLogRecordFacade record = A.Fake(); - A.CallTo(() => record.GetPropertyValues(A._)).Returns(new object[] { "141.98.9.20" }); + A.CallTo(() => record.GetPropertyValues(A._)).Returns(["141.98.9.20"]); IPAddress? actualAddress = null; listener.failure += (_, address) => actualAddress = address; @@ -106,7 +121,7 @@ public void builtInPattern() { public void customPattern() { EventLogRecordFacade record = A.Fake(); A.CallTo(() => record.Properties) - .Returns(new List { new("sshd: PID 29722: Failed password for invalid user root from 71.194.180.25 port 48316 ssh2") }); + .Returns([new EventPropertyFacade("sshd: PID 29722: Failed password for invalid user root from 71.194.180.25 port 48316 ssh2")]); IPAddress? actualAddress = null; listener.failure += (_, address) => actualAddress = address; @@ -131,7 +146,7 @@ public void logNameNotFoundSkipsSelector() { EventLogWatcherFacade watcher = A.Fake(); A.CallToSet(() => watcher.Enabled).Throws(); - new EventLogListenerImpl(invalidConfiguration, _ => watcher); + _ = new EventLogListenerImpl(invalidConfiguration, _ => watcher); A.CallTo(() => watcher.Dispose()).MustHaveHappened(); } @@ -149,7 +164,7 @@ public void logPermissionDeniedSkipsSelector() { EventLogWatcherFacade watcher = A.Fake(); A.CallToSet(() => watcher.Enabled).Throws(); - new EventLogListenerImpl(invalidConfiguration, _ => watcher); + _ = new EventLogListenerImpl(invalidConfiguration, _ => watcher); A.CallTo(() => watcher.Dispose()).MustHaveHappened(); } @@ -170,12 +185,12 @@ public void missingPatternGroupThrowsException() { [Fact] public void eventDataIndex() { EventLogRecordFacade record = A.Fake(); - A.CallTo(() => record.Properties).Returns(new List { - new("LogonDenied"), - new("Default Frontend WIN-EXCHANGE"), - new("Login"), - new("42.85.233.11") - }); + A.CallTo(() => record.Properties).Returns([ + new EventPropertyFacade("LogonDenied"), + new EventPropertyFacade("Default Frontend WIN-EXCHANGE"), + new EventPropertyFacade("Login"), + new EventPropertyFacade("42.85.233.11") + ]); IPAddress? actualAddress = null; listener.failure += (_, address) => actualAddress = address;