private void ParseRegex(Regex regex, string text, bool successful, string timestampFormat) { List <IPAddressLogEvent> events = new(); IPAddressEventType type = (successful ? IPAddressEventType.SuccessfulLogin : IPAddressEventType.FailedLogin); foreach (IPAddressLogEvent info in IPBanService.GetIPAddressEventsFromRegex(regex, text, timestampFormat, type, dns)) { info.Source ??= Source; // apply default source only if we don't already have a source if (info.FailedLoginThreshold <= 0) { info.FailedLoginThreshold = FailedLoginThreshold; } if (successful) { info.LogLevel = SuccessfulLogLevel; } else { info.LogLevel = FailedLogLevel; } events.Add(info); Logger.Debug("Log file found match, ip: {0}, user: {1}, source: {2}, count: {3}, type: {4}", info.IPAddress, info.UserName, info.Source, info.Count, info.Type); } loginHandler.AddIPAddressLogEvents(events); }
/// <summary> /// Constructor /// </summary> /// <param name="ipAddress">IP address</param> /// <param name="userName">User name</param> /// <param name="source">Source</param> /// <param name="count">How many messages were aggregated, 1 for no aggregation</param> /// <param name="type">Event type</param> /// <param name="timestamp">Timestamp of the event, default for current timestamp</param> public IPAddressLogEvent(string ipAddress, string userName, string source, int count, IPAddressEventType type, DateTime timestamp = default) { IPAddress = ipAddress; UserName = userName; Source = source; Count = count; Type = type; Timestamp = (timestamp == default ? IPBanService.UtcNow : timestamp); FoundMatch = true; }
/// <summary> /// Constructor /// </summary> /// <param name="ipAddress">IP address</param> /// <param name="userName">User name</param> /// <param name="source">Source</param> /// <param name="count">How many messages were aggregated, 1 for no aggregation</param> /// <param name="type">Event type</param> /// <param name="timestamp">Timestamp of the event, default for current timestamp</param> public IPAddressLogEvent(string ipAddress, string userName, string source, int count, IPAddressEventType type, DateTime timestamp = default) { // normalize ip address if possible if (System.Net.IPAddress.TryParse(ipAddress, out System.Net.IPAddress parsedIPAddress)) { IPAddress = parsedIPAddress.ToString(); } else { IPAddress = ipAddress; } UserName = userName; Source = source; Count = count; Type = type; Timestamp = (timestamp == default ? IPBanService.UtcNow : timestamp); }
/// <summary> /// Constructor /// </summary> /// <param name="ipAddress">IP address</param> /// <param name="userName">User name</param> /// <param name="source">Source</param> /// <param name="count">How many messages were aggregated, 1 for no aggregation</param> /// <param name="type">Event type</param> /// <param name="timestamp">Timestamp of the event, default for current timestamp</param> /// <param name="external">Whether this log came from an external source</param> /// <param name="failedLoginThreshold">Failed login threshold or 0 for default</param> /// <param name="logLevel">Log level when the event is logged</param> public IPAddressLogEvent(string ipAddress, string userName, string source, int count, IPAddressEventType type, DateTime timestamp = default, bool external = false, int failedLoginThreshold = 0, LogLevel logLevel = LogLevel.Warning) { // normalize ip address if possible if (System.Net.IPAddress.TryParse(ipAddress, out System.Net.IPAddress parsedIPAddress)) { IPAddress = parsedIPAddress.ToString(); } else { IPAddress = ipAddress; } UserName = userName; Source = source; Count = count; Type = type; Timestamp = (timestamp == default ? IPBanService.UtcNow : timestamp); External = external; FailedLoginThreshold = failedLoginThreshold; }
/// <summary> /// Get an ip address and user name out of text using regex. Regex may contain groups named source_[sourcename] to override the source. /// </summary> /// <param name="regex">Regex</param> /// <param name="text">Text</param> /// <param name="ipAddress">Found ip address or null if none</param> /// <param name="userName">Found user name or null if none</param> /// <param name="timestampFormat">Timestamp format</param> /// <param name="eventType">Event type</param> /// <param name="dns">Dns lookup to resolve ip addresses</param> /// <returns>Set of matches from text</returns> public static IEnumerable <IPAddressLogEvent> GetIPAddressEventsFromRegex(Regex regex, string text, string timestampFormat = null, IPAddressEventType eventType = IPAddressEventType.FailedLogin, IDnsLookup dns = null) { const string customSourcePrefix = "source_"; // if no regex or no text, we are done if (regex is null || string.IsNullOrWhiteSpace(text)) { yield break; } // remove control chars text = new string(text.Where(c => c == '\n' || c == '\t' || !char.IsControl(c)).ToArray()).Trim(); // go through all the matches and pull out event info foreach (Match match in regex.Matches(text)) { string userName = null; string ipAddress = null; string source = null; DateTime timestamp = default; // check for a user name Group userNameGroup = match.Groups["username"]; if (userNameGroup != null && userNameGroup.Success) { userName = (userName ?? userNameGroup.Value.Trim(regexTrimChars)); } // check for source Group sourceGroup = match.Groups["source"]; if (sourceGroup != null && sourceGroup.Success) { source = (source ?? sourceGroup.Value.Trim(regexTrimChars)); } // check for groups with a custom source name foreach (Group group in match.Groups) { if (group.Success && group.Name != null && string.IsNullOrWhiteSpace(source) && group.Name.StartsWith(customSourcePrefix)) { source = group.Name.Substring(customSourcePrefix.Length); } } // check for timestamp group Group timestampGroup = match.Groups["timestamp"]; if (timestampGroup != null && timestampGroup.Success) { string toParse = timestampGroup.Value.Trim(regexTrimChars); if (string.IsNullOrWhiteSpace(timestampFormat) || !DateTime.TryParseExact(toParse, timestampFormat.Trim(), CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal | DateTimeStyles.AdjustToUniversal, out timestamp)) { DateTime.TryParse(toParse, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal | DateTimeStyles.AdjustToUniversal, out timestamp); } } // check if the regex had an ipadddress group Group ipAddressGroup = match.Groups["ipaddress"]; if (ipAddressGroup is null) { ipAddressGroup = match.Groups["ipaddress_exact"]; } if (ipAddressGroup != null && ipAddressGroup.Success && !string.IsNullOrWhiteSpace(ipAddressGroup.Value)) { string tempIPAddress = ipAddressGroup.Value.Trim(); // in case of IP:PORT format, try a second time, stripping off the :PORT, saves having to do this in all // the different ip regex. int lastColon = tempIPAddress.LastIndexOf(':'); bool isValidIPAddress = IPAddress.TryParse(tempIPAddress, out IPAddress tmp); if (isValidIPAddress || (lastColon >= 0 && IPAddress.TryParse(tempIPAddress.Substring(0, lastColon), out tmp))) { ipAddress = tmp.ToString(); } // if we are parsing anything as ip address (including dns names) if (ipAddress is null && dns != null && ipAddressGroup.Name == "ipaddress" && tempIPAddress != Environment.MachineName && tempIPAddress != "-") { // Check Host by name Logger.Info("Parsing as IP failed, checking dns '{0}'", tempIPAddress); try { IPHostEntry entry = dns.GetHostEntryAsync(tempIPAddress).Sync(); if (entry != null && entry.AddressList != null && entry.AddressList.Length > 0) { ipAddress = entry.AddressList.FirstOrDefault().ToString(); Logger.Info("Dns result '{0}' = '{1}'", tempIPAddress, ipAddress); break; } } catch { Logger.Info("Parsing as dns failed '{0}'", tempIPAddress); } } } // see if there is a repeat indicator in the message int repeatCount = ExtractRepeatCount(match, text); // return an event for this match yield return(new IPAddressLogEvent(ipAddress, userName, source, repeatCount, eventType, timestamp)); } }
/// <summary> /// Get an ip address and user name out of text using regex. Regex may contain groups named source_[sourcename] to override the source. /// </summary> /// <param name="regex">Regex</param> /// <param name="text">Text</param> /// <param name="ipAddress">Found ip address or null if none</param> /// <param name="userName">Found user name or null if none</param> /// <param name="timestampFormat">Timestamp format</param> /// <param name="eventType">Event type</param> /// <param name="dns">Dns lookup to resolve ip addresses</param> /// <returns>Set of matches from text</returns> public static IEnumerable <IPAddressLogEvent> GetIPAddressEventsFromRegex(Regex regex, string text, string timestampFormat = null, IPAddressEventType eventType = IPAddressEventType.FailedLogin, IDnsLookup dns = null) { const string customSourcePrefix = "source_"; // if no regex or no text, we are done if (regex is null || string.IsNullOrWhiteSpace(text)) { yield break; } // remove control chars text = new string(text.Where(c => c == '\n' || c == '\t' || !char.IsControl(c)).ToArray()); // go through all the matches and pull out event info MatchCollection matches = regex.Matches(text); foreach (Match match in matches) { string userName = null; string ipAddress = null; string source = null; DateTime timestamp = default; // check for a user name Group userNameGroup = match.Groups["username"]; if (userNameGroup != null && userNameGroup.Success) { userName ??= userNameGroup.Value.Trim(regexTrimChars); } // check for source Group sourceGroup = match.Groups["source"]; if (sourceGroup != null && sourceGroup.Success) { source ??= sourceGroup.Value.Trim(regexTrimChars); } // check for groups with a custom source name foreach (Group group in match.Groups) { if (group.Success && group.Name != null && string.IsNullOrWhiteSpace(source) && group.Name.StartsWith(customSourcePrefix)) { source = group.Name[customSourcePrefix.Length..];
private async Task TestMultipleBanTimespansExternalBlockAsync(bool resetFailedLogin) { const string ipAddress = "99.99.99.99"; const string userName = "******"; const string source = "RDP"; const IPAddressEventType type = IPAddressEventType.FailedLogin; KeyValuePair <DateTime?, DateTime?> banDates; service.IPBanDelegate = new ExternalBlocker(service); IPAddressLogEvent[] events = new IPAddressLogEvent[1]; using IPBanConfig.TempConfigChanger configChanger = new IPBanConfig.TempConfigChanger(service, xml => { xml = IPBanConfig.ChangeConfigAppSetting(xml, "BanTime", "00:00:01:00,00:00:05:00,00:00:15:00,89:00:00:00"); xml = IPBanConfig.ChangeConfigAppSetting(xml, "ResetFailedLoginCountForUnbannedIPAddresses", resetFailedLogin.ToString()); return(xml); }, out string newConfig); Assert.AreEqual(4, service.Config.BanTimes.Length); // send a block event, should get banned for 1 minute IPBanService.UtcNow = new DateTime(2020, 1, 1, 1, 1, 1, DateTimeKind.Utc); for (int i = 0; i < 2; i++) { events[0] = new IPAddressLogEvent(ipAddress, userName, source, 1, type, IPBanService.UtcNow); service.AddIPAddressLogEvents(events); await service.RunCycle(); Assert.IsFalse(service.Firewall.IsIPAddressBlocked(ipAddress)); // run cycle again, should get pinged by external blocker and ip should be blocked await service.RunCycle(); Assert.IsTrue(service.Firewall.IsIPAddressBlocked(ipAddress)); Assert.IsTrue(service.DB.TryGetBanDates(ipAddress, out banDates)); Assert.AreEqual(IPBanService.UtcNow, banDates.Key); Assert.AreEqual(IPBanService.UtcNow.AddMinutes(1.0), banDates.Value); // short step, should still be blocked IPBanService.UtcNow += TimeSpan.FromSeconds(1.0); await service.RunCycle(); Assert.IsTrue(service.Firewall.IsIPAddressBlocked(ipAddress)); IPBanService.UtcNow += TimeSpan.FromMinutes(1.0); await service.RunCycle(); Assert.IsFalse(service.Firewall.IsIPAddressBlocked(ipAddress)); // send a fail login event, should get banned for 5 minutes events[0] = new IPAddressLogEvent(ipAddress, userName, source, 1, type, IPBanService.UtcNow); service.AddIPAddressLogEvents(events); await service.RunCycle(); Assert.IsFalse(service.Firewall.IsIPAddressBlocked(ipAddress)); DateTime savedBanDate = IPBanService.UtcNow; // add a failed and blocked login event, should not interfere with the ban cycle events[0] = new IPAddressLogEvent(ipAddress, userName, source, 1, IPAddressEventType.FailedLogin, IPBanService.UtcNow); service.AddIPAddressLogEvents(events); await service.RunCycle(); events[0] = new IPAddressLogEvent(ipAddress, userName, source, 1, IPAddressEventType.Blocked, IPBanService.UtcNow, true); service.AddIPAddressLogEvents(events); await service.RunCycle(); // throw in some chaos IPBanService.UtcNow += TimeSpan.FromSeconds(7.213); // blocker will ban the ip await service.RunCycle(); Assert.IsTrue(service.Firewall.IsIPAddressBlocked(ipAddress)); Assert.IsTrue(service.DB.TryGetBanDates(ipAddress, out banDates)); Assert.AreEqual(savedBanDate, banDates.Key); Assert.AreEqual(savedBanDate.AddMinutes(5.0), banDates.Value); IPBanService.UtcNow += TimeSpan.FromMinutes(20.0); await service.RunCycle(); Assert.IsFalse(service.Firewall.IsIPAddressBlocked(ipAddress)); // send a failed login event, should get banned for 15 minutes events[0] = new IPAddressLogEvent(ipAddress, userName, source, 1, type, IPBanService.UtcNow); service.AddIPAddressLogEvents(events); await service.RunCycle(); Assert.IsFalse(service.Firewall.IsIPAddressBlocked(ipAddress)); // cycle again, blocker will ban await service.RunCycle(); Assert.IsTrue(service.Firewall.IsIPAddressBlocked(ipAddress)); Assert.IsTrue(service.DB.TryGetBanDates(ipAddress, out banDates)); Assert.AreEqual(IPBanService.UtcNow, banDates.Key); Assert.AreEqual(IPBanService.UtcNow.AddMinutes(15.0), banDates.Value); IPBanService.UtcNow += TimeSpan.FromMinutes(30.0); await service.RunCycle(); Assert.IsFalse(service.Firewall.IsIPAddressBlocked(ipAddress)); // send a block event, should get banned for 89 days events[0] = new IPAddressLogEvent(ipAddress, userName, source, 1, type, IPBanService.UtcNow); service.AddIPAddressLogEvents(events); await service.RunCycle(); Assert.IsFalse(service.Firewall.IsIPAddressBlocked(ipAddress)); // cycle again, blocker will ban await service.RunCycle(); Assert.IsTrue(service.Firewall.IsIPAddressBlocked(ipAddress)); Assert.IsTrue(service.DB.TryGetBanDates(ipAddress, out banDates)); Assert.AreEqual(IPBanService.UtcNow, banDates.Key); Assert.AreEqual(IPBanService.UtcNow.AddDays(89.0), banDates.Value); IPBanService.UtcNow += TimeSpan.FromDays(91.0); await service.RunCycle(); Assert.IsFalse(service.Firewall.IsIPAddressBlocked(ipAddress)); } }