Skip to content

Commit

Permalink
User name truncation
Browse files Browse the repository at this point in the history
By default, all usernames will truncate at the '@' character. This can be configured differently if desired. I did some refactoring of regex parsing to move it out of the config class and into it's own class.
  • Loading branch information
jjxtra committed Feb 2, 2023
1 parent c7c3f73 commit 4d6ebd6
Show file tree
Hide file tree
Showing 17 changed files with 481 additions and 356 deletions.
144 changes: 11 additions & 133 deletions IPBanCore/Core/IPBan/IPBanConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,10 @@ MIT License
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml;
Expand All @@ -43,7 +45,7 @@ namespace DigitalRuby.IPBanCore
/// <summary>
/// Configuration for ip ban app
/// </summary>
public class IPBanConfig : IIsWhitelisted
public sealed class IPBanConfig : IIsWhitelisted
{
/// <summary>
/// Allow temporary change of config
Expand Down Expand Up @@ -124,6 +126,7 @@ public void Dispose()
private readonly bool clearBannedIPAddressesOnRestart;
private readonly bool clearFailedLoginsOnSuccessfulLogin;
private readonly bool processInternalIPAddresses;
private readonly string truncateUserNameChars;
private readonly HashSet<string> userNameWhitelist = new(StringComparer.Ordinal);
private readonly int userNameWhitelistMaximumEditDistance = 2;
private readonly Regex userNameWhitelistRegex;
Expand Down Expand Up @@ -190,6 +193,8 @@ private IPBanConfig(XmlDocument doc, IDnsLookup dns = null, IDnsServerList dnsLi
TryGetConfig<bool>("ClearBannedIPAddressesOnRestart", ref clearBannedIPAddressesOnRestart);
TryGetConfig<bool>("ClearFailedLoginsOnSuccessfulLogin", ref clearFailedLoginsOnSuccessfulLogin);
TryGetConfig<bool>("ProcessInternalIPAddresses", ref processInternalIPAddresses);
TryGetConfig<string>("TruncateUserNameChars", ref truncateUserNameChars);
IPBanRegexParser.Instance.TruncateUserNameChars = truncateUserNameChars;
GetConfig<TimeSpan>("ExpireTime", ref expireTime, TimeSpan.Zero, maxBanTimeSpan);
if (expireTime.TotalMinutes < 1.0)
{
Expand Down Expand Up @@ -403,138 +408,6 @@ private void ParseFirewallBlockRules()
}
}

/// <summary>
/// Validate a regex - returns an error otherwise empty string if success
/// </summary>
/// <param name="regex">Regex to validate, can be null or empty</param>
/// <param name="options">Regex options</param>
/// <param name="throwException">True to throw the exception instead of returning the string, false otherwise</param>
/// <returns>Null if success, otherwise an error string indicating the problem</returns>
public static string ValidateRegex(string regex, RegexOptions options = RegexOptions.IgnoreCase | RegexOptions.CultureInvariant, bool throwException = false)
{
try
{
if (regex != null)
{
_ = new Regex(regex, options);
}
return null;
}
catch (Exception ex)
{
if (throwException)
{
throw;
}
return ex.Message;
}
}

private static readonly Dictionary<string, Regex> regexCacheCompiled = new();
private static readonly Dictionary<string, Regex> regexCacheNotCompiled = new();

/// <summary>
/// Get a regex from text
/// </summary>
/// <param name="text">Text</param>
/// <param name="multiline">Whether to use multi-line regex, default is false which is single line</param>
/// <returns>Regex or null if text is null or whitespace</returns>
public static Regex ParseRegex(string text, bool multiline = false)
{
const int maxCacheSize = 200;

text = (text ?? string.Empty).Trim();
if (text.Length == 0)
{
return null;
}

string[] lines = text.Split('\n', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
StringBuilder sb = new();
foreach (string line in lines)
{
sb.Append(line);
}
RegexOptions options = RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Compiled;
if (multiline)
{
options |= RegexOptions.Multiline;
}
string sbText = sb.ToString();
string cacheKey = ((uint)options).ToString("X8") + ":" + sbText;

// allow up to maxCacheSize compiled dynamic regular expression, with minimal config changes/reload, this should last the lifetime of an app
lock (regexCacheCompiled)
{
if (regexCacheCompiled.TryGetValue(cacheKey, out Regex value))
{
return value;
}
else if (regexCacheCompiled.Count < maxCacheSize)
{
value = new Regex(sbText, options);
regexCacheCompiled.Add(cacheKey, value);
return value;
}
}

// have to fall-back to non-compiled regex to avoid run-away memory usage
try
{
lock (regexCacheNotCompiled)
{
if (regexCacheNotCompiled.TryGetValue(cacheKey, out Regex value))
{
return value;
}

// strip compiled flag
options &= (~RegexOptions.Compiled);
value = new Regex(sbText, options);
regexCacheNotCompiled.Add(cacheKey, value);
return value;
}
}
finally
{
// clear non-compield regex cache if it exceeds max size
lock (regexCacheNotCompiled)
{
if (regexCacheNotCompiled.Count > maxCacheSize)
{
regexCacheNotCompiled.Clear();
}
}
}
}

/// <summary>
/// Clean a multi-line string to make it more readable
/// </summary>
/// <param name="text">Multi-line string</param>
/// <returns>Cleaned multi-line string</returns>
public static string CleanMultilineString(string text)
{
text = (text ?? string.Empty).Trim();
if (text.Length == 0)
{
return string.Empty;
}

string[] lines = text.Split('\n', StringSplitOptions.RemoveEmptyEntries);
StringBuilder sb = new();
foreach (string line in lines)
{
string trimmedLine = line.Trim();
if (trimmedLine.Length != 0)
{
sb.Append(trimmedLine);
sb.Append('\n');
}
}
return sb.ToString().Trim();
}

/// <inheritdoc />
public override string ToString()
{
Expand Down Expand Up @@ -1005,6 +878,11 @@ public static XmlDocument MergeXml(string xmlBase, string xmlOverride)
/// </summary>
public IIPBanFilter BlacklistFilter => blacklistFilter;

/// <summary>
/// Characters to truncate user names at, empty for no truncation
/// </summary>
public string TruncateUserNameChars { get { return truncateUserNameChars; } }

/// <summary>
/// White list user names. Any user name found not in the list is banned, unless the list is empty, in which case no checking is done.
/// If not empty, Any user name within 'UserNameWhitelistMinimumEditDistance' in the config is also not banned.
Expand Down
4 changes: 2 additions & 2 deletions IPBanCore/Core/IPBan/IPBanConfigWindowsEventViewer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ public string XPath
public XmlCData Regex
{
get => regex;
set => RegexObject = IPBanConfig.ParseRegex(regex = value);
set => RegexObject = IPBanRegexParser.ParseRegex(regex = value);
}

/// <summary>
Expand Down Expand Up @@ -186,7 +186,7 @@ public void SetExpressionsFromExpressionsText()
}

Expressions.Clear();
string[] lines = IPBanConfig.CleanMultilineString(ExpressionsText).Split('\n');
string[] lines = IPBanRegexParser.CleanMultilineString(ExpressionsText).Split('\n');
string line;
EventViewerExpression currentExpression = null;
for (int i = 0; i < lines.Length; i++)
Expand Down
2 changes: 1 addition & 1 deletion IPBanCore/Core/IPBan/IPBanFilter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ private bool IsNonIPMatch(string entry)

if (!string.IsNullOrWhiteSpace(regexValue))
{
regex = IPBanConfig.ParseRegex(regexValue);
regex = IPBanRegexParser.ParseRegex(regexValue);
}
}

Expand Down
4 changes: 2 additions & 2 deletions IPBanCore/Core/IPBan/IPBanLogFileManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,8 @@ private void UpdateLogFiles(IPBanConfig newConfig)
MaxFileSizeBytes = newFile.MaxFileSize,
PathAndMask = pathAndMask,
PingIntervalMilliseconds = (service.ManualCycle ? 0 : newFile.PingInterval),
RegexFailure = IPBanConfig.ParseRegex(newFile.FailedLoginRegex, true),
RegexSuccess = IPBanConfig.ParseRegex(newFile.SuccessfulLoginRegex, true),
RegexFailure = IPBanRegexParser.ParseRegex(newFile.FailedLoginRegex, true),
RegexSuccess = IPBanRegexParser.ParseRegex(newFile.SuccessfulLoginRegex, true),
RegexFailureTimestampFormat = newFile.FailedLoginRegexTimestampFormat,
RegexSuccessTimestampFormat = newFile.SuccessfulLoginRegexTimestampFormat,
Source = newFile.Source,
Expand Down
2 changes: 1 addition & 1 deletion IPBanCore/Core/IPBan/IPBanLogFileScanner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ private void ParseRegex(Regex regex, string text, bool successful, string timest
{
List<IPAddressLogEvent> events = new();
IPAddressEventType type = (successful ? IPAddressEventType.SuccessfulLogin : IPAddressEventType.FailedLogin);
foreach (IPAddressLogEvent info in IPBanService.GetIPAddressEventsFromRegex(regex, text, timestampFormat, type, Source, dns))
foreach (IPAddressLogEvent info in IPBanRegexParser.Instance.GetIPAddressEventsFromRegex(regex, text, timestampFormat, type, Source, dns))
{
info.Source ??= Source; // apply default source only if we don't already have a source
if (info.FailedLoginThreshold <= 0)
Expand Down
4 changes: 2 additions & 2 deletions IPBanCore/Core/IPBan/IPBanLogFileTester.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,9 @@ public void AddIPAddressLogEvents(IEnumerable<IPAddressLogEvent> events)
MaxFileSizeBytes = 0,
PathAndMask = fileName.Trim(),
PingIntervalMilliseconds = 0,
RegexFailure = (File.Exists(regexFailureFile) && regexFailureFile.Length > 2 ? IPBanConfig.ParseRegex(File.ReadAllText(regexFailureFile)) : null),
RegexFailure = (File.Exists(regexFailureFile) && regexFailureFile.Length > 2 ? IPBanRegexParser.ParseRegex(File.ReadAllText(regexFailureFile)) : null),
RegexFailureTimestampFormat = regexFailureTimestampFormat.Trim('.'),
RegexSuccess = (File.Exists(regexSuccessFile) && regexSuccessFile.Length > 2 ? IPBanConfig.ParseRegex(File.ReadAllText(regexSuccessFile)) : null),
RegexSuccess = (File.Exists(regexSuccessFile) && regexSuccessFile.Length > 2 ? IPBanRegexParser.ParseRegex(File.ReadAllText(regexSuccessFile)) : null),
RegexSuccessTimestampFormat = regexSuccessTimestampFormat.Trim('.'),
Source = "test",
SuccessfulLogLevel = LogLevel.Warning
Expand Down

0 comments on commit 4d6ebd6

Please sign in to comment.