From: Alex Savage Date: Mon, 24 Mar 2025 23:41:18 +0000 (+0000) Subject: pop3: app-layer parser using sawp-pop3 X-Git-Tag: suricata-8.0.0-beta1~11 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=778053876b0d67594f2d28888f2c3ddcd4747dd7;p=thirdparty%2Fsuricata.git pop3: app-layer parser using sawp-pop3 This module uses the sawp-pop3 crate to parse POP3 requests and responses Features: - eve logging - events for parsable but non-RFC-compliant messages Ticket: 3243 --- diff --git a/doc/userguide/output/eve/eve-json-format.rst b/doc/userguide/output/eve/eve-json-format.rst index 0693c3aa1e..f9acec8315 100644 --- a/doc/userguide/output/eve/eve-json-format.rst +++ b/doc/userguide/output/eve/eve-json-format.rst @@ -3099,3 +3099,35 @@ Example of ARP logging: request and response "dest_mac": "00:1d:09:f0:92:ab", "dest_ip": "10.10.10.1" } + +Event type: POP3 +---------------- + +Fields +~~~~~~ + +* "request" (optional): a request sent by the pop3 client + * "request.command" (string): a pop3 command, for example "USER" or "STAT", if unknown but valid `UnknownCommand` event will be set + * "request.args" (array of strings): pop3 command arguments, if incorrect number for command `IncorrectArgumentCount` event will be set +* "response" (optional): a response sent by the pop3 server + * "response.success" (boolean): whether the response is successful, ie. +OK + * "response.status" (string): the response status, one of "OK" or "ERR" + * "response.header" (string): the content of the first line of the reponse + * "response.data" (array of strings): the response data, which may contain multiple lines + +Example of POP3 logging: + +:: + + "pop3": { + "request": { + "command": "USER", + "args": ["user@example.com"], + }, + "response": { + "success": true, + "status": "OK", + "header": "+OK password required for \"user@example.com\"", + "data": [] + } + } diff --git a/etc/schema.json b/etc/schema.json index c0b4718d94..0bdb0178c5 100644 --- a/etc/schema.json +++ b/etc/schema.json @@ -3795,6 +3795,52 @@ }, "additionalProperties": false }, + "pop3": { + "type": "object", + "optional": true, + "properties": { + "request": { + "type": "object", + "optional": true, + "properties": { + "command": { + "description": "a pop3 command, for example `USER` or `STAT`", + "type": "string" + }, + "args": { + "description": "pop3 request arguments", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "response": { + "type": "object", + "optional": true, + "properties": { + "success": { + "description": "response indicated positive status ie +OK", + "type": "boolean" + }, + "status": { + "type": "string" + }, + "header": { + "description": "first line of response", + "type": "string" + }, + "data": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + }, "quic": { "type": "object", "optional": true, diff --git a/rules/README.md b/rules/README.md index 93cee89f11..24fc53aa53 100644 --- a/rules/README.md +++ b/rules/README.md @@ -28,6 +28,7 @@ signature IDs. | TLS | 2230000 | 2230999 | | QUIC | 2231000 | 2231999 | | FTP | 2232000 | 2232999 | +| POP3 | 2236000 | 2236999 | | DNS | 2240000 | 2240999 | | PGSQL | 2241000 | 2241999 | | MODBUS | 2250000 | 2250999 | diff --git a/rules/pop3-events.rules b/rules/pop3-events.rules new file mode 100644 index 0000000000..0a76767947 --- /dev/null +++ b/rules/pop3-events.rules @@ -0,0 +1,9 @@ +# POP3 app-layer event rules +# +# SID's fall in the 2236000+ range. See https://redmine.openinfosecfoundation.org/projects/suricata/wiki/AppLayer +# +alert pop3 any any -> any any (msg:"SURICATA POP3 Too many transactions"; app-layer-event:pop3.too_many_transactions; sid:2236000; rev:1;) +alert pop3 any any -> any any (msg:"SURICATA POP3 Request Too Long"; app-layer-event:pop3.request_too_long; flow:to_server; sid:2236001; rev:1;) +alert pop3 any any -> any any (msg:"SURICATA POP3 Incorrect Argument Count"; app-layer-event:pop3.incorrect_argument_count; flow:to_server; sid:2236002; rev:1;) +alert pop3 any any -> any any (msg:"SURICATA POP3 Unknown Command"; app-layer-event:pop3.unknown_command; flow:to_server, sid:2236003; rev:1;) +alert pop3 any any -> any any (msg:"SURICATA POP3 Response Too Long"; app-layer-event:pop3.response_too_long; flow:to_client; sid:2236004; rev:1;) diff --git a/rust/Cargo.lock.in b/rust/Cargo.lock.in index 437867cc26..c17801cb7a 100644 --- a/rust/Cargo.lock.in +++ b/rust/Cargo.lock.in @@ -1396,6 +1396,17 @@ dependencies = [ "sawp-flags", ] +[[package]] +name = "sawp-pop3" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef576aa71811070479508d762e810d036b775f3629970901d082b4c9085d96da" +dependencies = [ + "nom", + "sawp", + "sawp-flags", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -1560,6 +1571,7 @@ dependencies = [ "regex", "sawp", "sawp-modbus", + "sawp-pop3", "sha1", "sha2", "snmp-parser", diff --git a/rust/Cargo.toml.in b/rust/Cargo.toml.in index 79603374a4..693b80cec1 100644 --- a/rust/Cargo.toml.in +++ b/rust/Cargo.toml.in @@ -59,6 +59,7 @@ der-parser = { version = "~9.0.0", default-features = false } kerberos-parser = { version = "~0.8.0", default-features = false } sawp-modbus = "~0.13.1" +sawp-pop3 = "~0.13.1" sawp = "~0.13.1" ntp-parser = "~0.6.0" ipsec-parser = "~0.7.0" diff --git a/rust/src/lib.rs b/rust/src/lib.rs index ba2cdb9c8e..d1908989d8 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -119,6 +119,7 @@ pub mod pgsql; pub mod telnet; pub mod websocket; pub mod enip; +pub mod pop3; pub mod applayertemplate; pub mod rdp; pub mod x509; diff --git a/rust/src/pop3/logger.rs b/rust/src/pop3/logger.rs new file mode 100644 index 0000000000..93be446a95 --- /dev/null +++ b/rust/src/pop3/logger.rs @@ -0,0 +1,63 @@ +/* Copyright (C) 2025 Open Information Security Foundation + * + * You can copy, redistribute or modify this Program under the terms of + * the GNU General Public License version 2 as published by the Free + * Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * version 2 along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +// Author: Alex Savage + +//! POP3 parser json logger + +use super::pop3::POP3Transaction; +use crate::jsonbuilder::{JsonBuilder, JsonError}; +use std; + +fn log_pop3(tx: &POP3Transaction, js: &mut JsonBuilder) -> Result<(), JsonError> { + js.open_object("pop3")?; + if let Some(ref request) = tx.request { + let js_request = js.open_object("request")?; + js_request.set_string("command", &request.keyword.to_string())?; + + let js_args = js_request.open_array("args")?; + for arg in &request.args { + js_args.append_string_from_bytes(arg)?; + } + js_args.close()?; + js_request.close()?; + } + if let Some(ref response) = tx.response { + let js_response = js.open_object("response")?; + js_response.set_bool("success", response.status == sawp_pop3::Status::OK)?; + js_response.set_string("status", response.status.to_str())?; + js_response.set_string_from_bytes("header", &response.header)?; + + let js_data = js_response.open_array("data")?; + for data in &response.data { + js_data.append_string_from_bytes(data)?; + } + js_data.close()?; + js_response.close()?; + } + + js.close()?; + Ok(()) +} + +#[no_mangle] +pub unsafe extern "C" fn SCPop3LoggerLog( + tx: *mut std::os::raw::c_void, js: &mut JsonBuilder, +) -> bool { + let tx = cast_pointer!(tx, POP3Transaction); + log_pop3(tx, js).is_ok() +} diff --git a/rust/src/pop3/mod.rs b/rust/src/pop3/mod.rs new file mode 100644 index 0000000000..ecdd540bc3 --- /dev/null +++ b/rust/src/pop3/mod.rs @@ -0,0 +1,21 @@ +/* Copyright (C) 2025 Open Information Security Foundation + * + * You can copy, redistribute or modify this Program under the terms of + * the GNU General Public License version 2 as published by the Free + * Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * version 2 along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +//! Application layer pop3 parser and logger module. + +pub mod logger; +pub mod pop3; diff --git a/rust/src/pop3/pop3.rs b/rust/src/pop3/pop3.rs new file mode 100644 index 0000000000..d6f82a78b3 --- /dev/null +++ b/rust/src/pop3/pop3.rs @@ -0,0 +1,541 @@ +/* Copyright (C) 2025 Open Information Security Foundation + * + * You can copy, redistribute or modify this Program under the terms of + * the GNU General Public License version 2 as published by the Free + * Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * version 2 along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +// Author: Alex Savage + +//! POP3 parser + +use crate::applayer::*; +use crate::conf::{conf_get, get_memval}; +use crate::core::{ALPROTO_FAILED, ALPROTO_UNKNOWN, IPPROTO_TCP}; +use crate::flow::Flow; +use std; +use std::collections::VecDeque; +use std::ffi::CString; +use std::os::raw::{c_char, c_int, c_void}; +use suricata_sys::sys::AppProto; + +use sawp::error::Error as SawpError; +use sawp::error::ErrorKind as SawpErrorKind; +use sawp::parser::Direction; +use sawp::parser::Parse; +use sawp::probe::Probe; +use sawp::probe::Status; +use sawp_pop3::{self, Command, ErrorFlag, Flag, Flags, InnerMessage, Response}; + +static mut POP3_MAX_TX: usize = 256; + +pub(super) static mut ALPROTO_POP3: AppProto = ALPROTO_UNKNOWN; +const POP3_PARSER: sawp_pop3::POP3 = sawp_pop3::POP3 {}; + +#[derive(AppLayerEvent)] +enum POP3Event { + TooManyTransactions, + /// Command + space + argument + CRLF must not exceed 255 octets (RFC 2449) + RequestTooLong, + /// Number of arguments doesn't match the command + IncorrectArgumentCount, + /// Correct command format, but unknown value + UnknownCommand, + /// First line of server response + CRLF must not exceed 512 octets (RFC 2449) + ResponseTooLong, +} + +impl From for POP3Event { + fn from(flag: ErrorFlag) -> Self { + match flag { + ErrorFlag::CommandTooLong => POP3Event::RequestTooLong, + ErrorFlag::IncorrectArgumentNum => POP3Event::IncorrectArgumentCount, + ErrorFlag::UnknownKeyword => POP3Event::UnknownCommand, + ErrorFlag::ResponseTooLong => POP3Event::ResponseTooLong, + } + } +} + +pub struct POP3Transaction { + tx_id: u64, + pub request: Option, + pub response: Option, + complete: bool, + + tx_data: AppLayerTxData, +} + +impl POP3Transaction { + pub fn new(tx_id: u64) -> POP3Transaction { + Self { + tx_id, + request: None, + response: None, + complete: false, + tx_data: AppLayerTxData::new(), + } + } + + fn error_flags_to_events(&mut self, flags: Flags) { + for val in ErrorFlag::ITEMS + .iter() + .filter(move |&flag| flags.contains(*flag)) + { + self.tx_data.set_event(POP3Event::from(*val) as u8); + } + } +} + +impl Transaction for POP3Transaction { + fn id(&self) -> u64 { + self.tx_id + } +} + +#[derive(Default)] +pub struct POP3State { + state_data: AppLayerStateData, + tx_id: u64, + transactions: VecDeque, + request_gap: bool, + response_gap: bool, +} + +impl State for POP3State { + fn get_transaction_count(&self) -> usize { + self.transactions.len() + } + + fn get_transaction_by_index(&self, index: usize) -> Option<&POP3Transaction> { + self.transactions.get(index) + } +} + +impl POP3State { + pub fn new() -> Self { + Default::default() + } + + // Free a transaction by ID. + fn free_tx(&mut self, tx_id: u64) { + if let Some(index) = self.transactions.iter().position(|tx| tx.id() == tx_id + 1) { + self.transactions.remove(index); + } + } + + pub fn get_tx(&self, tx_id: u64) -> Option<&POP3Transaction> { + self.transactions.iter().find(|tx| tx.tx_id == tx_id + 1) + } + + pub fn get_tx_mut(&mut self, tx_id: u64) -> Option<&mut POP3Transaction> { + self.transactions + .iter_mut() + .find(|tx| tx.tx_id == tx_id + 1) + } + + fn new_tx(&mut self) -> Option { + if self.transactions.len() > unsafe { POP3_MAX_TX } { + for tx_old in &mut self.transactions { + if !tx_old.complete { + tx_old.tx_data.updated_tc = true; + tx_old.tx_data.updated_ts = true; + tx_old.complete = true; + tx_old + .tx_data + .set_event(POP3Event::TooManyTransactions as u8); + } + } + return None; + } + + self.tx_id += 1; + Some(POP3Transaction::new(self.tx_id)) + } + + fn find_request(&mut self) -> Option<&mut POP3Transaction> { + self.transactions + .iter_mut() + .find(|tx| tx.response.is_none()) + } + + fn parse_request(&mut self, input: &[u8]) -> AppLayerResult { + // We're not interested in empty requests. + if input.is_empty() { + return AppLayerResult::ok(); + } + + // If there was gap, check we can sync up again. + if self.request_gap { + unsafe { + if probe(input, Direction::ToServer) != ALPROTO_POP3 { + // The parser now needs to decide what to do as we are not in sync. + // For this pop3, we'll just try again next time. + return AppLayerResult::ok(); + } + } + + // It looks like we're in sync with a message header, clear gap + // state and keep parsing. + self.request_gap = false; + } + + let mut start = input; + while !start.is_empty() { + match POP3_PARSER.parse(start, Direction::ToServer) { + Ok((rem, Some(msg))) => { + if let InnerMessage::Command(command) = msg.inner { + let mut tx = match self.new_tx() { + Some(tx) => tx, + None => return AppLayerResult::err(), + }; + + tx.error_flags_to_events(msg.error_flags); + tx.request = Some(command); + self.transactions.push_back(tx); + } + + start = rem; + } + Ok((rem, None)) => { + // Not enough data. This parser doesn't give us a good indication + // of how much data is missing so just ask for one more byte so + // parse is called as soon as more data is received. + + let consumed = input.len() - rem.len(); + let needed = rem.len() + 1; + return AppLayerResult::incomplete(consumed as u32, needed as u32); + } + Err(SawpError { + kind: SawpErrorKind::Incomplete(sawp::error::Needed::Size(needed)), + }) => { + let consumed = input.len() - start.len(); + let needed = start.len() + needed.get(); + return AppLayerResult::incomplete(consumed as u32, needed as u32); + } + Err(SawpError { + kind: SawpErrorKind::Incomplete(sawp::error::Needed::Unknown), + }) => { + let consumed = input.len() - start.len(); + let needed = start.len() + 1; + return AppLayerResult::incomplete(consumed as u32, needed as u32); + } + Err(_) => return AppLayerResult::err(), + } + } + + // Input was fully consumed. + return AppLayerResult::ok(); + } + + fn parse_response(&mut self, input: &[u8], flow: *const Flow) -> AppLayerResult { + // We're not interested in empty responses. + if input.is_empty() { + return AppLayerResult::ok(); + } + + if self.response_gap { + unsafe { + if probe(input, Direction::ToClient) != ALPROTO_POP3 { + // The parser now needs to decide what to do as we are not in sync. + // For this pop3, we'll just try again next time. + return AppLayerResult::ok(); + } + } + + // It looks like we're in sync with a message header, clear gap + // state and keep parsing. + self.response_gap = false; + } + let mut start = input; + while !start.is_empty() { + match POP3_PARSER.parse(start, Direction::ToClient) { + Ok((rem, Some(msg))) => { + if let InnerMessage::Response(mut response) = msg.inner { + let tx = if let Some(tx) = self.find_request() { + tx + } else { + // Server sends banner before any requests + let tx = match self.new_tx() { + Some(tx) => tx, + None => return AppLayerResult::err(), + }; + + let tx_id = tx.id(); + self.transactions.push_back(tx); + self.get_tx_mut(tx_id - 1).unwrap() + }; + + tx.error_flags_to_events(msg.error_flags); + tx.complete = true; + + if response.status == sawp_pop3::Status::OK && tx.request.is_some() { + let command = tx.request.as_ref().unwrap(); + match &command.keyword { + sawp_pop3::Keyword::STLS => { + unsafe { + AppLayerRequestProtocolTLSUpgrade(flow); + }; + } + sawp_pop3::Keyword::RETR => { + // Don't hold onto the whole email body + + // TODO: pass off to mime parser + response.data.clear(); + } + _ => {} + } + } + tx.response = Some(response); + } + start = rem; + } + Ok((rem, None)) => { + // Not enough data. This parser doesn't give us a good indication + // of how much data is missing so just ask for one more byte so + // parse is called as soon as more data is received. + + let consumed = input.len() - rem.len(); + let needed = rem.len() + 1; + return AppLayerResult::incomplete(consumed as u32, needed as u32); + } + Err(SawpError { + kind: SawpErrorKind::Incomplete(sawp::error::Needed::Size(needed)), + }) => { + let consumed = input.len() - start.len(); + let needed = start.len() + needed.get(); + return AppLayerResult::incomplete(consumed as u32, needed as u32); + } + Err(SawpError { + kind: SawpErrorKind::Incomplete(sawp::error::Needed::Unknown), + }) => { + let consumed = input.len() - start.len(); + let needed = start.len() + 1; + return AppLayerResult::incomplete(consumed as u32, needed as u32); + } + Err(_) => { + return AppLayerResult::err(); + } + } + } + + // All input was fully consumed. + return AppLayerResult::ok(); + } + + fn on_request_gap(&mut self) { + self.request_gap = true; + } + + fn on_response_gap(&mut self) { + self.response_gap = true; + } +} + +/// Reasonably need at least 5 bytes to determine +/// 3 bytes for short commands like 'TOP' or response '+OK' +/// and 2 bytes for the CRLF. +static MIN_PROBE_LEN: u32 = 5; + +/// Probe for a command or response +fn probe(input: &[u8], direction: Direction) -> AppProto { + match POP3_PARSER.probe(input, direction) { + Status::Recognized => unsafe { ALPROTO_POP3 }, + Status::Incomplete => ALPROTO_UNKNOWN, + Status::Unrecognized => ALPROTO_FAILED, + } +} + +// C exports. + +/// C entry point for a probing parser. +unsafe extern "C" fn pop3_probe_tc( + _flow: *const Flow, _direction: u8, input: *const u8, input_len: u32, _rdir: *mut u8, +) -> AppProto { + if input_len < MIN_PROBE_LEN { + ALPROTO_UNKNOWN + } else { + let slice = build_slice!(input, input_len as usize); + probe(slice, Direction::ToClient) + } +} + +unsafe extern "C" fn pop3_probe_ts( + _flow: *const Flow, _direction: u8, input: *const u8, input_len: u32, _rdir: *mut u8, +) -> AppProto { + if input_len < MIN_PROBE_LEN { + ALPROTO_UNKNOWN + } else { + let slice = build_slice!(input, input_len as usize); + probe(slice, Direction::ToServer) + } +} + +extern "C" fn pop3_state_new(_orig_state: *mut c_void, _orig_proto: AppProto) -> *mut c_void { + let state = POP3State::new(); + let boxed = Box::new(state); + return Box::into_raw(boxed) as *mut c_void; +} + +unsafe extern "C" fn pop3_state_free(state: *mut c_void) { + std::mem::drop(Box::from_raw(state as *mut POP3State)); +} + +unsafe extern "C" fn pop3_state_tx_free(state: *mut c_void, tx_id: u64) { + let state = cast_pointer!(state, POP3State); + state.free_tx(tx_id); +} + +unsafe extern "C" fn pop3_parse_request( + _flow: *const Flow, state: *mut c_void, pstate: *mut c_void, stream_slice: StreamSlice, + _data: *const c_void, +) -> AppLayerResult { + let eof = AppLayerParserStateIssetFlag(pstate, APP_LAYER_PARSER_EOF_TS) > 0; + + if eof { + // If needed, handle EOF, or pass it into the parser. + return AppLayerResult::ok(); + } + + let state = cast_pointer!(state, POP3State); + + if stream_slice.is_gap() { + // Here we have a gap signaled by the input being null, but a greater + // than 0 input_len which provides the size of the gap. + state.on_request_gap(); + AppLayerResult::ok() + } else { + let buf = stream_slice.as_slice(); + state.parse_request(buf) + } +} + +unsafe extern "C" fn pop3_parse_response( + flow: *const Flow, state: *mut c_void, pstate: *mut c_void, stream_slice: StreamSlice, + _data: *const c_void, +) -> AppLayerResult { + let _eof = AppLayerParserStateIssetFlag(pstate, APP_LAYER_PARSER_EOF_TC) > 0; + let state = cast_pointer!(state, POP3State); + + if stream_slice.is_gap() { + // Here we have a gap signaled by the input being null, but a greater + // than 0 input_len which provides the size of the gap. + state.on_response_gap(); + AppLayerResult::ok() + } else { + let buf = stream_slice.as_slice(); + state.parse_response(buf, flow) + } +} + +unsafe extern "C" fn pop3_state_get_tx(state: *mut c_void, tx_id: u64) -> *mut c_void { + let state = cast_pointer!(state, POP3State); + match state.get_tx(tx_id) { + Some(tx) => { + return tx as *const _ as *mut _; + } + None => { + return std::ptr::null_mut(); + } + } +} + +unsafe extern "C" fn pop3_state_get_tx_count(state: *mut c_void) -> u64 { + let state = cast_pointer!(state, POP3State); + return state.tx_id; +} + +unsafe extern "C" fn pop3_tx_get_alstate_progress(tx: *mut c_void, direction: u8) -> c_int { + let tx = cast_pointer!(tx, POP3Transaction); + if direction == Direction::ToServer as u8 { + (tx.request.is_some() || tx.complete) as c_int + } else { + (tx.response.is_some() || tx.complete) as c_int + } +} + +export_tx_data_get!(pop3_get_tx_data, POP3Transaction); +export_state_data_get!(pop3_get_state_data, POP3State); + +// Parser name as a C style string. +const PARSER_NAME: &[u8] = b"pop3\0"; + +#[no_mangle] +pub unsafe extern "C" fn SCRegisterPop3Parser() { + let default_port = CString::new("[110]").unwrap(); + let parser = RustParser { + name: PARSER_NAME.as_ptr() as *const c_char, + default_port: default_port.as_ptr(), + ipproto: IPPROTO_TCP, + probe_ts: Some(pop3_probe_ts), + probe_tc: Some(pop3_probe_tc), + min_depth: 0, + max_depth: 16, + state_new: pop3_state_new, + state_free: pop3_state_free, + tx_free: pop3_state_tx_free, + parse_ts: pop3_parse_request, + parse_tc: pop3_parse_response, + get_tx_count: pop3_state_get_tx_count, + get_tx: pop3_state_get_tx, + tx_comp_st_ts: 1, + tx_comp_st_tc: 1, + tx_get_progress: pop3_tx_get_alstate_progress, + get_eventinfo: Some(POP3Event::get_event_info), + get_eventinfo_byid: Some(POP3Event::get_event_info_by_id), + localstorage_new: None, + localstorage_free: None, + get_tx_files: None, + get_tx_iterator: Some(state_get_tx_iterator::), + get_tx_data: pop3_get_tx_data, + get_state_data: pop3_get_state_data, + apply_tx_config: None, + flags: APP_LAYER_PARSER_OPT_ACCEPT_GAPS, + get_frame_id_by_name: None, + get_frame_name_by_id: None, + get_state_id_by_name: None, + get_state_name_by_id: None, + }; + + let ip_proto_str = CString::new("tcp").unwrap(); + + if AppLayerProtoDetectConfProtoDetectionEnabled(ip_proto_str.as_ptr(), parser.name) != 0 { + let alproto = AppLayerRegisterProtocolDetection(&parser, 1); + ALPROTO_POP3 = alproto; + if AppLayerParserConfParserEnabled(ip_proto_str.as_ptr(), parser.name) != 0 { + let _ = AppLayerRegisterParser(&parser, alproto); + } + let retval = conf_get("app-layer.protocols.pop3.stream-depth"); + if let Some(val) = retval { + match get_memval(val) { + Ok(retval) => { + let stream_depth = retval as u32; + AppLayerParserSetStreamDepth(IPPROTO_TCP, ALPROTO_POP3, stream_depth); + } + Err(_) => { + SCLogError!("Invalid depth value"); + } + } + } + if let Some(val) = conf_get("app-layer.protocols.pop3.max-tx") { + if let Ok(v) = val.parse::() { + POP3_MAX_TX = v; + } else { + SCLogError!("Invalid value for pop3.max-tx"); + } + } + AppLayerParserRegisterLogger(IPPROTO_TCP, ALPROTO_POP3); + SCLogDebug!("Rust pop3 parser registered."); + } else { + SCLogDebug!("Protocol detector and parser disabled for POP3."); + } +} diff --git a/src/app-layer-parser.c b/src/app-layer-parser.c index 088dd1f0f5..5b2b619860 100644 --- a/src/app-layer-parser.c +++ b/src/app-layer-parser.c @@ -1812,21 +1812,12 @@ void AppLayerParserRegisterProtocolParsers(void) SCRfbRegisterParser(); SCMqttRegisterParser(); SCRegisterPgsqlParser(); + SCRegisterPop3Parser(); SCRegisterRdpParser(); RegisterHTTP2Parsers(); rs_telnet_register_parser(); RegisterIMAPParsers(); - /** POP3 */ - AppLayerProtoDetectRegisterProtocol(ALPROTO_POP3, "pop3"); - if (AppLayerProtoDetectConfProtoDetectionEnabled("tcp", "pop3")) { - if (AppLayerProtoDetectPMRegisterPatternCS( - IPPROTO_TCP, ALPROTO_POP3, "+OK ", 4, 0, STREAM_TOCLIENT) < 0) { - FatalError("pop3 proto registration failure"); - } - } else { - SCLogInfo("Protocol detection and parser disabled for pop3 protocol."); - } for (size_t i = 0; i < preregistered_callbacks_nb; i++) { PreRegisteredCallbacks[i](); } diff --git a/src/output.c b/src/output.c index d9833cefda..11ae93c3f0 100644 --- a/src/output.c +++ b/src/output.c @@ -908,6 +908,7 @@ void OutputRegisterRootLoggers(void) // ALPROTO_DHCP TODO missing RegisterSimpleJsonApplayerLogger(ALPROTO_SIP, (EveJsonSimpleTxLogFunc)rs_sip_log_json, NULL); RegisterSimpleJsonApplayerLogger(ALPROTO_RFB, (EveJsonSimpleTxLogFunc)rs_rfb_logger_log, NULL); + RegisterSimpleJsonApplayerLogger(ALPROTO_POP3, (EveJsonSimpleTxLogFunc)SCPop3LoggerLog, NULL); RegisterSimpleJsonApplayerLogger( ALPROTO_MQTT, (EveJsonSimpleTxLogFunc)JsonMQTTAddMetadata, NULL); RegisterSimpleJsonApplayerLogger( @@ -1138,6 +1139,10 @@ void OutputRegisterLoggers(void) JsonLogThreadDeinit); /* DoH2 JSON logger. */ JsonDoh2LogRegister(); + /* POP3 JSON logger */ + OutputRegisterTxSubModule(LOGGER_JSON_TX, "eve-log", "JsonPop3Log", "eve-log.pop3", + OutputJsonLogInitSub, ALPROTO_POP3, JsonGenericDirFlowLogger, JsonLogThreadInit, + JsonLogThreadDeinit); /* Template JSON logger. */ OutputRegisterTxSubModule(LOGGER_JSON_TX, "eve-log", "JsonTemplateLog", "eve-log.template", OutputJsonLogInitSub, ALPROTO_TEMPLATE, JsonGenericDirPacketLogger, JsonLogThreadInit, diff --git a/suricata.yaml.in b/suricata.yaml.in index 28d539ac3a..66e121b12c 100644 --- a/suricata.yaml.in +++ b/suricata.yaml.in @@ -334,6 +334,7 @@ outputs: - sip - quic - ldap + - pop3 - arp: enabled: no # Many events can be logged. Disabled by default - dhcp: @@ -1034,7 +1035,13 @@ app-layer: imap: enabled: detection-only pop3: - enabled: detection-only + enabled: yes + detection-ports: + dp: 110 + # Stream reassembly size for POP3. By default, track it completely. + stream-depth: 0 + # Maximum number of live POP3 transactions per flow + # max-tx: 256 smb: enabled: yes detection-ports: