From 581533b09c0b1ca4faadaa0eb9d521de8553f3fa Mon Sep 17 00:00:00 2001 From: mdecimus Date: Tue, 8 Oct 2024 11:48:03 +0200 Subject: [PATCH] Detect and ban port scanners as well as other forms of abuse (closes #820) --- crates/common/src/listener/blocked.rs | 91 ++++++++++++++++++-- crates/common/src/manager/config.rs | 32 ++++--- crates/common/src/telemetry/metrics/store.rs | 3 +- crates/imap/src/core/client.rs | 29 +++++++ crates/jmap/src/api/http.rs | 72 +++++++++++----- crates/managesieve/src/core/client.rs | 30 ++++++- crates/pop3/src/client.rs | 23 ++++- crates/smtp/src/inbound/rcpt.rs | 31 +++++-- crates/smtp/src/inbound/session.rs | 28 +++++- crates/trc/src/event/description.rs | 8 +- crates/trc/src/event/mod.rs | 6 ++ crates/trc/src/lib.rs | 3 +- crates/trc/src/serializers/binary.rs | 6 +- tests/src/jmap/enterprise.rs | 3 +- 14 files changed, 303 insertions(+), 62 deletions(-) diff --git a/crates/common/src/listener/blocked.rs b/crates/common/src/listener/blocked.rs index ffb57b3b..40720b72 100644 --- a/crates/common/src/listener/blocked.rs +++ b/crates/common/src/listener/blocked.rs @@ -7,13 +7,16 @@ use std::{fmt::Debug, net::IpAddr}; use ahash::AHashSet; -use utils::config::{ - ipmask::{IpAddrMask, IpAddrOrMask}, - utils::ParseValue, - Config, ConfigKey, Rate, +use utils::{ + config::{ + ipmask::{IpAddrMask, IpAddrOrMask}, + utils::ParseValue, + Config, ConfigKey, Rate, + }, + glob::GlobPattern, }; -use crate::Server; +use crate::{manager::config::MatchType, Server}; #[derive(Debug, Clone)] pub struct Security { @@ -24,6 +27,9 @@ pub struct Security { allowed_ip_networks: Vec, has_allowed_networks: bool, + http_banned_paths: Vec, + scanner_fail_rate: Option, + auth_fail_rate: Option, rcpt_fail_rate: Option, loiter_fail_rate: Option, @@ -71,6 +77,39 @@ impl Security { let blocked = BlockedIps::parse(config); + // Parse blocked HTTP paths + let mut http_banned_paths = config + .values("server.fail2ban.http-banned-paths") + .filter_map(|(_, v)| { + let v = v.trim(); + if !v.is_empty() { + MatchType::parse(v).into() + } else { + None + } + }) + .collect::>(); + if http_banned_paths.is_empty() { + for pattern in [ + "*.php*", + "*.cgi*", + "*.asp*", + "*/wp-*", + "*/php*", + "*/cgi-bin*", + "*xmlrpc*", + "*../*", + "*/..*", + "*joomla*", + "*wordpress*", + "*drupal*", + ] + .iter() + { + http_banned_paths.push(MatchType::Matches(GlobPattern::compile(pattern, true))); + } + } + Security { has_blocked_networks: !blocked.blocked_ip_networks.is_empty(), blocked_ip_networks: blocked.blocked_ip_networks, @@ -86,17 +125,43 @@ impl Security { loiter_fail_rate: config .property_or_default::>("server.fail2ban.loitering", "150/1d") .unwrap_or_default(), + http_banned_paths, + scanner_fail_rate: config + .property_or_default::>("server.fail2ban.scanner", "30/1d") + .unwrap_or_default(), } } } impl Server { - pub async fn is_rcpt_fail2banned(&self, ip: IpAddr) -> trc::Result { + pub async fn is_rcpt_fail2banned(&self, ip: IpAddr, rcpt: &str) -> trc::Result { if let Some(rate) = &self.core.network.security.rcpt_fail_rate { + let is_allowed = self.is_ip_allowed(&ip) + || (self + .lookup_store() + .is_rate_allowed(format!("r:{ip}").as_bytes(), rate, false) + .await? + .is_none() + && self + .lookup_store() + .is_rate_allowed(format!("r:{rcpt}").as_bytes(), rate, false) + .await? + .is_none()); + + if !is_allowed { + return self.block_ip(ip).await.map(|_| true); + } + } + + Ok(false) + } + + pub async fn is_scanner_fail2banned(&self, ip: IpAddr) -> trc::Result { + if let Some(rate) = &self.core.network.security.scanner_fail_rate { let is_allowed = self.is_ip_allowed(&ip) || self .lookup_store() - .is_rate_allowed(format!("r:{ip}").as_bytes(), rate, false) + .is_rate_allowed(format!("h:{ip}").as_bytes(), rate, false) .await? .is_none(); @@ -108,6 +173,16 @@ impl Server { Ok(false) } + pub async fn is_http_banned_path(&self, path: &str, ip: IpAddr) -> trc::Result { + let paths = &self.core.network.security.http_banned_paths; + + if !paths.is_empty() && paths.iter().any(|p| p.matches(path)) && !self.is_ip_allowed(&ip) { + self.block_ip(ip).await.map(|_| true) + } else { + Ok(false) + } + } + pub async fn is_loiter_fail2banned(&self, ip: IpAddr) -> trc::Result { if let Some(rate) = &self.core.network.security.loiter_fail_rate { let is_allowed = self.is_ip_allowed(&ip) @@ -253,6 +328,8 @@ impl Default for Security { auth_fail_rate: Default::default(), rcpt_fail_rate: Default::default(), loiter_fail_rate: Default::default(), + scanner_fail_rate: Default::default(), + http_banned_paths: Default::default(), } } } diff --git a/crates/common/src/manager/config.rs b/crates/common/src/manager/config.rs index 4cc5bae8..07b2668d 100644 --- a/crates/common/src/manager/config.rs +++ b/crates/common/src/manager/config.rs @@ -40,8 +40,8 @@ enum Pattern { Exclude(MatchType), } -#[derive(Debug)] -enum MatchType { +#[derive(Debug, Clone)] +pub enum MatchType { Equal(String), StartsWith(String), EndsWith(String), @@ -469,17 +469,7 @@ impl Patterns { if value.is_empty() { continue; } - let match_type = if value == "*" { - MatchType::All - } else if let Some(value) = value.strip_suffix('*') { - MatchType::StartsWith(value.to_string()) - } else if let Some(value) = value.strip_prefix('*') { - MatchType::EndsWith(value.to_string()) - } else if value.contains('*') { - MatchType::Matches(GlobPattern::compile(&value, false)) - } else { - MatchType::Equal(value.to_string()) - }; + let match_type = MatchType::parse(&value); cfg_local_patterns.push(if is_include { Pattern::Include(match_type) @@ -541,7 +531,21 @@ impl Patterns { } impl MatchType { - fn matches(&self, value: &str) -> bool { + pub fn parse(value: &str) -> Self { + if value == "*" { + MatchType::All + } else if let Some(value) = value.strip_suffix('*') { + MatchType::StartsWith(value.to_string()) + } else if let Some(value) = value.strip_prefix('*') { + MatchType::EndsWith(value.to_string()) + } else if value.contains('*') { + MatchType::Matches(GlobPattern::compile(value, false)) + } else { + MatchType::Equal(value.to_string()) + } + } + + pub fn matches(&self, value: &str) -> bool { match self { MatchType::Equal(pattern) => value == pattern, MatchType::StartsWith(pattern) => value.starts_with(pattern), diff --git a/crates/common/src/telemetry/metrics/store.rs b/crates/common/src/telemetry/metrics/store.rs index 1510b4de..4af40f3e 100644 --- a/crates/common/src/telemetry/metrics/store.rs +++ b/crates/common/src/telemetry/metrics/store.rs @@ -106,7 +106,8 @@ impl MetricsStore for Store { EventType::MessageIngest(MessageIngestEvent::Spam), EventType::Auth(AuthEvent::Failed), EventType::Security(SecurityEvent::AuthenticationBan), - EventType::Security(SecurityEvent::BruteForceBan), + EventType::Security(SecurityEvent::ScanBan), + EventType::Security(SecurityEvent::AbuseBan), EventType::Security(SecurityEvent::LoiterBan), EventType::Security(SecurityEvent::IpBlocked), EventType::IncomingReport(IncomingReportEvent::DmarcReport), diff --git a/crates/imap/src/core/client.rs b/crates/imap/src/core/client.rs index a6bea4e1..a4b4b1b0 100644 --- a/crates/imap/src/core/client.rs +++ b/crates/imap/src/core/client.rs @@ -14,6 +14,7 @@ use imap_proto::{ receiver::{self, Request}, Command, ResponseType, StatusResponse, }; +use trc::SecurityEvent; use super::{SelectedMailbox, Session, SessionData, State}; @@ -50,6 +51,34 @@ impl Session { break; } Err(receiver::Error::Error { response }) => { + // Check for port scanners + if matches!( + (&self.state, response.key(trc::Key::Code)), + ( + State::NotAuthenticated { .. }, + Some(trc::Value::Static("PARSE")) + ) + ) { + match self.server.is_scanner_fail2banned(self.remote_addr).await { + Ok(true) => { + trc::event!( + Security(SecurityEvent::ScanBan), + SpanId = self.session_id, + RemoteIp = self.remote_addr, + Reason = "Invalid IMAP command", + ); + + return SessionResult::Close; + } + Ok(false) => {} + Err(err) => { + trc::error!(err + .span_id(self.session_id) + .details("Failed to check for fail2ban")); + } + } + } + if !self.write_error(response).await { return SessionResult::Close; } diff --git a/crates/jmap/src/api/http.rs b/crates/jmap/src/api/http.rs index 86d8984a..c91ed8f4 100644 --- a/crates/jmap/src/api/http.rs +++ b/crates/jmap/src/api/http.rs @@ -32,6 +32,7 @@ use jmap_proto::{ types::{blob::BlobId, id::Id}, }; use std::future::Future; +use trc::SecurityEvent; use crate::{ api::management::enterprise::telemetry::TelemetryApi, @@ -253,12 +254,12 @@ impl ParseHttp for Server { } } ("mta-sts.txt", &Method::GET) => { - if let Some(policy) = self.build_mta_sts_policy() { - return Ok(Resource::new("text/plain", policy.to_string().into_bytes()) - .into_http_response()); + return if let Some(policy) = self.build_mta_sts_policy() { + Ok(Resource::new("text/plain", policy.to_string().into_bytes()) + .into_http_response()) } else { - return Err(trc::ResourceEvent::NotFound.into_err()); - } + Err(trc::ResourceEvent::NotFound.into_err()) + }; } ("mail-v1.xml", &Method::GET) => { return self.handle_autoconfig_request(&req).await; @@ -471,11 +472,9 @@ impl ParseHttp for Server { let resource = self.inner.data.webadmin.get("logo.svg").await?; - return if !resource.is_empty() { - Ok(resource.into_http_response()) - } else { - Err(trc::ResourceEvent::NotFound.into_err()) - }; + if !resource.is_empty() { + return Ok(resource.into_http_response()); + } // SPDX-SnippetEnd } @@ -507,14 +506,23 @@ impl ParseHttp for Server { .get(path.strip_prefix('/').unwrap_or(path)) .await?; - return if !resource.is_empty() { - Ok(resource.into_http_response()) - } else { - Err(trc::ResourceEvent::NotFound.into_err()) - }; + if !resource.is_empty() { + return Ok(resource.into_http_response()); + } } } + // Block dangerous URLs + let path = req.uri().path(); + if self.is_http_banned_path(path, session.remote_ip).await? { + trc::event!( + Security(SecurityEvent::ScanBan), + SpanId = session.session_id, + RemoteIp = session.remote_ip, + Path = path.to_string(), + ); + } + Err(trc::ResourceEvent::NotFound.into_err()) } } @@ -652,11 +660,32 @@ async fn handle_session(inner: Arc, session: SessionDat .with_upgrades() .await { - trc::event!( - Http(trc::HttpEvent::Error), - SpanId = session.session_id, - Reason = http_err.to_string(), - ); + match inner + .build_server() + .is_scanner_fail2banned(session.remote_ip) + .await + { + Ok(true) => { + trc::event!( + Security(SecurityEvent::ScanBan), + SpanId = session.session_id, + RemoteIp = session.remote_ip, + Reason = http_err.to_string(), + ); + } + Ok(false) => { + trc::event!( + Http(trc::HttpEvent::Error), + SpanId = session.session_id, + Reason = http_err.to_string(), + ); + } + Err(err) => { + trc::error!(err + .span_id(session.session_id) + .details("Failed to check for fail2ban")); + } + } } } @@ -994,7 +1023,8 @@ impl ToRequestError for trc::Error { }, trc::EventType::Security(cause) => match cause { trc::SecurityEvent::AuthenticationBan - | trc::SecurityEvent::BruteForceBan + | trc::SecurityEvent::ScanBan + | trc::SecurityEvent::AbuseBan | trc::SecurityEvent::LoiterBan | trc::SecurityEvent::IpBlocked => RequestError::too_many_auth_attempts(), trc::SecurityEvent::Unauthorized => RequestError::forbidden(), diff --git a/crates/managesieve/src/core/client.rs b/crates/managesieve/src/core/client.rs index 87603b7e..159669a0 100644 --- a/crates/managesieve/src/core/client.rs +++ b/crates/managesieve/src/core/client.rs @@ -9,7 +9,7 @@ use imap_proto::receiver::{self, Request}; use jmap_proto::types::{collection::Collection, property::Property}; use store::query::Filter; use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; -use trc::AddContext; +use trc::{AddContext, SecurityEvent}; use super::{Command, ResponseCode, SerializeResponse, Session, State}; @@ -46,6 +46,34 @@ impl Session { break; } Err(receiver::Error::Error { response }) => { + // Check for port scanners + if matches!( + (&self.state, response.key(trc::Key::Code)), + ( + State::NotAuthenticated { .. }, + Some(trc::Value::Static("PARSE")) + ) + ) { + match self.server.is_scanner_fail2banned(self.remote_addr).await { + Ok(true) => { + trc::event!( + Security(SecurityEvent::ScanBan), + SpanId = self.session_id, + RemoteIp = self.remote_addr, + Reason = "Invalid ManageSieve command", + ); + + return SessionResult::Close; + } + Ok(false) => {} + Err(err) => { + trc::error!(err + .span_id(self.session_id) + .details("Failed to check for fail2ban")); + } + } + } + if let Err(err) = self.write_error(response).await { trc::error!(err.span_id(self.session_id)); return SessionResult::Close; diff --git a/crates/pop3/src/client.rs b/crates/pop3/src/client.rs index ccc6f88c..b1648c9a 100644 --- a/crates/pop3/src/client.rs +++ b/crates/pop3/src/client.rs @@ -6,7 +6,7 @@ use common::listener::{SessionResult, SessionStream}; use mail_send::Credentials; -use trc::AddContext; +use trc::{AddContext, SecurityEvent}; use crate::{ protocol::{request::Error, Command, Mechanism}, @@ -49,6 +49,27 @@ impl Session { break; } Err(Error::Parse(err)) => { + // Check for port scanners + if matches!(&self.state, State::NotAuthenticated { .. },) { + match self.server.is_scanner_fail2banned(self.remote_addr).await { + Ok(true) => { + trc::event!( + Security(SecurityEvent::ScanBan), + SpanId = self.session_id, + RemoteIp = self.remote_addr, + Reason = "Invalid POP3 command", + ); + + return SessionResult::Close; + } + Ok(false) => {} + Err(err) => { + trc::error!(err + .span_id(self.session_id) + .details("Failed to check for fail2ban")); + } + } + } requests.push(Err(trc::Pop3Event::Error.into_err().details(err))); } } diff --git a/crates/smtp/src/inbound/rcpt.rs b/crates/smtp/src/inbound/rcpt.rs index 6da5b48b..03c831ce 100644 --- a/crates/smtp/src/inbound/rcpt.rs +++ b/crates/smtp/src/inbound/rcpt.rs @@ -209,9 +209,12 @@ impl Session { To = rcpt.address_lcase.clone(), ); - self.data.rcpt_to.pop(); + let rcpt_to = self.data.rcpt_to.pop().unwrap().address_lcase; return self - .rcpt_error(b"550 5.1.2 Mailbox does not exist.\r\n") + .rcpt_error( + b"550 5.1.2 Mailbox does not exist.\r\n", + rcpt_to, + ) .await; } } @@ -243,8 +246,10 @@ impl Session { To = rcpt.address_lcase.clone(), ); - self.data.rcpt_to.pop(); - return self.rcpt_error(b"550 5.1.2 Relay not allowed.\r\n").await; + let rcpt_to = self.data.rcpt_to.pop().unwrap().address_lcase; + return self + .rcpt_error(b"550 5.1.2 Relay not allowed.\r\n", rcpt_to) + .await; } } Err(err) => { @@ -275,8 +280,10 @@ impl Session { To = rcpt.address_lcase.clone(), ); - self.data.rcpt_to.pop(); - return self.rcpt_error(b"550 5.1.2 Relay not allowed.\r\n").await; + let rcpt_to = self.data.rcpt_to.pop().unwrap().address_lcase; + return self + .rcpt_error(b"550 5.1.2 Relay not allowed.\r\n", rcpt_to) + .await; } if self.is_allowed().await { @@ -301,17 +308,22 @@ impl Session { self.write(b"250 2.1.5 OK\r\n").await } - async fn rcpt_error(&mut self, response: &[u8]) -> Result<(), ()> { + async fn rcpt_error(&mut self, response: &[u8], rcpt: String) -> Result<(), ()> { tokio::time::sleep(self.params.rcpt_errors_wait).await; self.data.rcpt_errors += 1; let has_too_many_errors = self.data.rcpt_errors >= self.params.rcpt_errors_max; - match self.server.is_rcpt_fail2banned(self.data.remote_ip).await { + match self + .server + .is_rcpt_fail2banned(self.data.remote_ip, &rcpt) + .await + { Ok(true) => { trc::event!( - Security(SecurityEvent::BruteForceBan), + Security(SecurityEvent::AbuseBan), SpanId = self.data.session_id, RemoteIp = self.data.remote_ip, + To = rcpt, ); } Ok(false) => { @@ -320,6 +332,7 @@ impl Session { Smtp(SmtpEvent::TooManyInvalidRcpt), SpanId = self.data.session_id, Limit = self.params.rcpt_errors_max, + To = rcpt, ); } } diff --git a/crates/smtp/src/inbound/session.rs b/crates/smtp/src/inbound/session.rs index f0d1afc5..413ccfec 100644 --- a/crates/smtp/src/inbound/session.rs +++ b/crates/smtp/src/inbound/session.rs @@ -17,7 +17,7 @@ use smtp_proto::{ *, }; use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; -use trc::{NetworkEvent, SmtpEvent}; +use trc::{NetworkEvent, SecurityEvent, SmtpEvent}; use crate::core::{Session, State}; @@ -236,6 +236,32 @@ impl Session { Err(err) => match err { Error::NeedsMoreData { .. } => break 'outer, Error::UnknownCommand | Error::InvalidResponse { .. } => { + // Check for port scanners + if !self.is_authenticated() { + match self + .server + .is_scanner_fail2banned(self.data.remote_ip) + .await + { + Ok(true) => { + trc::event!( + Security(SecurityEvent::ScanBan), + SpanId = self.data.session_id, + RemoteIp = self.data.remote_ip, + Reason = "Invalid SMTP command", + ); + + return Err(()); + } + Ok(false) => {} + Err(err) => { + trc::error!(err + .span_id(self.data.session_id) + .details("Failed to check for fail2ban")); + } + } + } + trc::event!( Smtp(SmtpEvent::InvalidCommand), SpanId = self.data.session_id, diff --git a/crates/trc/src/event/description.rs b/crates/trc/src/event/description.rs index 07fca954..a74e194e 100644 --- a/crates/trc/src/event/description.rs +++ b/crates/trc/src/event/description.rs @@ -1787,9 +1787,10 @@ impl SecurityEvent { pub fn description(&self) -> &'static str { match self { SecurityEvent::AuthenticationBan => "Banned due to authentication errors", - SecurityEvent::BruteForceBan => "Banned due to brute force attack", + SecurityEvent::AbuseBan => "Banned due to abuse", SecurityEvent::LoiterBan => "Banned due to loitering", SecurityEvent::IpBlocked => "Blocked IP address", + SecurityEvent::ScanBan => "Banned due to scan", SecurityEvent::Unauthorized => "Unauthorized access", } } @@ -1799,9 +1800,10 @@ impl SecurityEvent { SecurityEvent::AuthenticationBan => { "IP address was banned due to multiple authentication errors" } - SecurityEvent::BruteForceBan => { - "IP address was banned due to possible brute force attack" + SecurityEvent::AbuseBan => { + "IP address was banned due to abuse, such as RCPT TO attacks" } + SecurityEvent::ScanBan => "IP address was banned due to exploit scanning", SecurityEvent::LoiterBan => "IP address was banned due to multiple loitering events", SecurityEvent::IpBlocked => "Rejected connection from blocked IP address", SecurityEvent::Unauthorized => "Account does not have permission to access resource", diff --git a/crates/trc/src/event/mod.rs b/crates/trc/src/event/mod.rs index 008d4959..d614e274 100644 --- a/crates/trc/src/event/mod.rs +++ b/crates/trc/src/event/mod.rs @@ -140,6 +140,12 @@ impl Event { self.inner == EventType::Store(StoreEvent::AssertValueFailed) } + pub fn key(&self, key: Key) -> Option<&Value> { + self.keys + .iter() + .find_map(|(k, v)| if *k == key { Some(v) } else { None }) + } + #[inline(always)] pub fn is_jmap_method_error(&self) -> bool { !matches!( diff --git a/crates/trc/src/lib.rs b/crates/trc/src/lib.rs index 1bf7b0af..39f35fd4 100644 --- a/crates/trc/src/lib.rs +++ b/crates/trc/src/lib.rs @@ -200,7 +200,8 @@ pub enum HttpEvent { #[event_type] pub enum SecurityEvent { AuthenticationBan, - BruteForceBan, + AbuseBan, + ScanBan, LoiterBan, IpBlocked, Unauthorized, diff --git a/crates/trc/src/serializers/binary.rs b/crates/trc/src/serializers/binary.rs index 12182bf9..f3776810 100644 --- a/crates/trc/src/serializers/binary.rs +++ b/crates/trc/src/serializers/binary.rs @@ -854,7 +854,7 @@ impl EventType { EventType::Tls(TlsEvent::NoCertificatesAvailable) => 546, EventType::Tls(TlsEvent::NotConfigured) => 547, EventType::Telemetry(TelemetryEvent::Alert) => 548, - EventType::Security(SecurityEvent::BruteForceBan) => 549, + EventType::Security(SecurityEvent::AbuseBan) => 549, EventType::Security(SecurityEvent::LoiterBan) => 550, EventType::Smtp(SmtpEvent::MailFromNotAllowed) => 551, EventType::Security(SecurityEvent::Unauthorized) => 552, @@ -863,6 +863,7 @@ impl EventType { EventType::Auth(AuthEvent::ClientRegistration) => 555, EventType::Ai(AiEvent::LlmResponse) => 556, EventType::Ai(AiEvent::ApiError) => 557, + EventType::Security(SecurityEvent::ScanBan) => 558, } } @@ -1457,7 +1458,7 @@ impl EventType { 546 => Some(EventType::Tls(TlsEvent::NoCertificatesAvailable)), 547 => Some(EventType::Tls(TlsEvent::NotConfigured)), 548 => Some(EventType::Telemetry(TelemetryEvent::Alert)), - 549 => Some(EventType::Security(SecurityEvent::BruteForceBan)), + 549 => Some(EventType::Security(SecurityEvent::AbuseBan)), 550 => Some(EventType::Security(SecurityEvent::LoiterBan)), 551 => Some(EventType::Smtp(SmtpEvent::MailFromNotAllowed)), 552 => Some(EventType::Security(SecurityEvent::Unauthorized)), @@ -1466,6 +1467,7 @@ impl EventType { 555 => Some(EventType::Auth(AuthEvent::ClientRegistration)), 556 => Some(EventType::Ai(AiEvent::LlmResponse)), 557 => Some(EventType::Ai(AiEvent::ApiError)), + 558 => Some(EventType::Security(SecurityEvent::ScanBan)), _ => None, } } diff --git a/tests/src/jmap/enterprise.rs b/tests/src/jmap/enterprise.rs index b5bdb3a2..e446a283 100644 --- a/tests/src/jmap/enterprise.rs +++ b/tests/src/jmap/enterprise.rs @@ -459,7 +459,8 @@ pub async fn insert_test_metrics(core: Arc) { EventType::MessageIngest(MessageIngestEvent::Spam), EventType::Auth(AuthEvent::Failed), EventType::Security(SecurityEvent::AuthenticationBan), - EventType::Security(SecurityEvent::BruteForceBan), + EventType::Security(SecurityEvent::ScanBan), + EventType::Security(SecurityEvent::AbuseBan), EventType::Security(SecurityEvent::LoiterBan), EventType::Security(SecurityEvent::IpBlocked), EventType::IncomingReport(IncomingReportEvent::DmarcReport),