mirror of
https://github.com/stalwartlabs/mail-server.git
synced 2025-09-08 21:14:13 +08:00
Detect and ban port scanners as well as other forms of abuse (closes #820)
This commit is contained in:
parent
1561a603ab
commit
581533b09c
14 changed files with 303 additions and 62 deletions
|
@ -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<IpAddrMask>,
|
||||
has_allowed_networks: bool,
|
||||
|
||||
http_banned_paths: Vec<MatchType>,
|
||||
scanner_fail_rate: Option<Rate>,
|
||||
|
||||
auth_fail_rate: Option<Rate>,
|
||||
rcpt_fail_rate: Option<Rate>,
|
||||
loiter_fail_rate: Option<Rate>,
|
||||
|
@ -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::<Vec<_>>();
|
||||
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::<Option<Rate>>("server.fail2ban.loitering", "150/1d")
|
||||
.unwrap_or_default(),
|
||||
http_banned_paths,
|
||||
scanner_fail_rate: config
|
||||
.property_or_default::<Option<Rate>>("server.fail2ban.scanner", "30/1d")
|
||||
.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Server {
|
||||
pub async fn is_rcpt_fail2banned(&self, ip: IpAddr) -> trc::Result<bool> {
|
||||
pub async fn is_rcpt_fail2banned(&self, ip: IpAddr, rcpt: &str) -> trc::Result<bool> {
|
||||
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<bool> {
|
||||
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<bool> {
|
||||
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<bool> {
|
||||
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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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<T: SessionStream> Session<T> {
|
|||
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;
|
||||
}
|
||||
|
|
|
@ -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<T: SessionStream>(inner: Arc<Inner>, 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(),
|
||||
|
|
|
@ -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<T: SessionStream> Session<T> {
|
|||
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;
|
||||
|
|
|
@ -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<T: SessionStream> Session<T> {
|
|||
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)));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -209,9 +209,12 @@ impl<T: SessionStream> Session<T> {
|
|||
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<T: SessionStream> Session<T> {
|
|||
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<T: SessionStream> Session<T> {
|
|||
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<T: SessionStream> Session<T> {
|
|||
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<T: SessionStream> Session<T> {
|
|||
Smtp(SmtpEvent::TooManyInvalidRcpt),
|
||||
SpanId = self.data.session_id,
|
||||
Limit = self.params.rcpt_errors_max,
|
||||
To = rcpt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<T: SessionStream> Session<T> {
|
|||
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,
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -140,6 +140,12 @@ impl Event<EventType> {
|
|||
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!(
|
||||
|
|
|
@ -200,7 +200,8 @@ pub enum HttpEvent {
|
|||
#[event_type]
|
||||
pub enum SecurityEvent {
|
||||
AuthenticationBan,
|
||||
BruteForceBan,
|
||||
AbuseBan,
|
||||
ScanBan,
|
||||
LoiterBan,
|
||||
IpBlocked,
|
||||
Unauthorized,
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -459,7 +459,8 @@ pub async fn insert_test_metrics(core: Arc<Core>) {
|
|||
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),
|
||||
|
|
Loading…
Add table
Reference in a new issue