diff --git a/rust/src/applayerpgsql/logger.rs b/rust/src/applayerpgsql/logger.rs new file mode 100644 index 000000000000..bc62628f9a78 --- /dev/null +++ b/rust/src/applayerpgsql/logger.rs @@ -0,0 +1,36 @@ +/* Copyright (C) 2018 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. + */ + +use std; +use crate::jsonbuilder::{JsonBuilder, JsonError}; +use super::pgsql::PGSQLTransaction; + +fn log_pgsql(tx: &PGSQLTransaction, js: &mut JsonBuilder) -> Result<(), JsonError> { + if let Some(ref request) = tx.request { + js.set_string("request", request)?; + } + if let Some(ref response) = tx.response { + js.set_string("response", response)?; + } + Ok(()) +} + +#[no_mangle] +pub extern "C" fn rs_pgsql_logger_log(tx: *mut std::os::raw::c_void, js: &mut JsonBuilder) -> bool { + let tx = cast_pointer!(tx, PGSQLTransaction); + log_pgsql(tx, js).is_ok() +} diff --git a/rust/src/applayerpgsql/mod.rs b/rust/src/applayerpgsql/mod.rs new file mode 100644 index 000000000000..188418637915 --- /dev/null +++ b/rust/src/applayerpgsql/mod.rs @@ -0,0 +1,20 @@ +/* Copyright (C) 2018 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. + */ + +pub mod pgsql; +pub mod logger; +mod parser; diff --git a/rust/src/applayerpgsql/parser.rs b/rust/src/applayerpgsql/parser.rs new file mode 100644 index 000000000000..f6e2bbd7dbcc --- /dev/null +++ b/rust/src/applayerpgsql/parser.rs @@ -0,0 +1,155 @@ +/* Copyright (C) 2021 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. + */ + + use std::{fmt, self, u16, u8}; + use nom::{number::complete::{be_u16, be_u32}}; + + // Dummy protocol major version, used for SSL, GSSAPI or Cancellation requests + pub const PGSQL_DUMMY_PROTO_MAJOR: u16 = 1234; + + // Dummy protocol minor version, used for SSL encryption requests + pub const PGSQL_DUMMY_PROTO_MINOR_SSL: u16 = 5679; + + #[derive(Debug, PartialEq)] + pub enum StartupMessageType { + StartupMessage = 0, + SslRequest = 1, + GssApiRequest = 2, + } + +impl fmt::Display for StartupMessageType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + StartupMessageType::StartupMessage => f.write_str("Startup Message"), + StartupMessageType::SslRequest => f.write_str("SSL Request"), + StartupMessageType::GssApiRequest => f.write_str("GSSAPI Request"), + } + } +} + + #[derive(Debug, PartialEq)] + pub struct PgsqlGenericStartupMessage{ + message_type: StartupMessageType, + length: u32, + proto_major: u16, + proto_minor: u16, + } + + named!(pub parse_ssl_request, + do_parse!( + len: verify!(be_u32, |&x| x == 8 ) + >> proto_major: verify!(be_u16, |&x| x == PGSQL_DUMMY_PROTO_MAJOR ) + >> proto_minor: verify!(be_u16, |&x| x == PGSQL_DUMMY_PROTO_MINOR_SSL ) + >> (PgsqlGenericStartupMessage { + message_type: StartupMessageType::SslRequest, + length: len, + proto_major: proto_major, + proto_minor: proto_minor, + }) + )); + + pub const PGSQL_FLAG_SSL_NEGATIVE_RESPONSE: u8 = 'N' as u8; + pub const PGSQL_FLAG_SSL_POSITIVE_RESPONSE: u8 = 'S' as u8; + + #[derive(Debug, PartialEq)] + pub struct PgsqlSslResponse { + message_type: u8, + } + +named!(pub parse_ssl_response, + do_parse!( + identifier: complete!(alt!(char!('N') | char!('S'))) + >> (PgsqlSslResponse{ + message_type: identifier as u8, + }) + )); + +fn parse_len(input: &str) -> Result { + input.parse::() +} + +named!(pub parse_message, + do_parse!( + len: map_res!( + map_res!(take_until!(":"), std::str::from_utf8), parse_len) >> + _sep: take!(1) >> + msg: take_str!(len) >> + ( + msg.to_string() + ) + )); + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn test_parse_pgsql_ssl_request() { + + let buf: &[u8] = &[0x00, 0x00, 0x00, 0x08, 0x04, 0xd2, 0x16, 0x2f]; + let ok_res = PgsqlGenericStartupMessage { + message_type: StartupMessageType::SslRequest, + length: 8, + proto_major: PGSQL_DUMMY_PROTO_MAJOR, + proto_minor: PGSQL_DUMMY_PROTO_MINOR_SSL, + }; + let (_remainder, res) = parse_ssl_request(&buf).unwrap(); + assert_eq!(res, ok_res); + + let buf: &[u8] = &[0x00, 0x00, 0x00, 0x08, 0x04, 0xd2, 0x16, 0x30]; + let res = parse_ssl_request(&buf); + assert!(res.is_err()); + + let buf: &[u8] = &[0x00, 0x00, 0x00, 0x01, 0x04, 0xd2, 0x16, 0x2f]; + let res = parse_ssl_request(&buf); + assert!(res.is_err()); + + let buf: &[u8] = &[0x00, 0x00, 0x00, 0x08, 0x04, 0xd2, 0x16]; + let res = parse_ssl_request(&buf); + assert!(res.is_err()); + } + + #[test] + fn test_parse_pgsql_ssl_response() { + // 'S' + let input: &[u8] = &[0x53]; + + let ok_res = PgsqlSslResponse { + message_type: PGSQL_FLAG_SSL_POSITIVE_RESPONSE, + }; + + let (_remainder, result) = parse_ssl_response(&input).unwrap(); + assert_eq!(result, ok_res); + + // 'N' + let input: &[u8] = &[0x4e]; + + let ok_res = PgsqlSslResponse { + message_type: PGSQL_FLAG_SSL_NEGATIVE_RESPONSE, + }; + + let (_remainder, result) = parse_ssl_response(&input).unwrap(); + assert_eq!(result, ok_res); + + // 'O' -- invalid option + let input: &[u8] = &[0x4f]; + let result = parse_ssl_response(&input); + assert!(result.is_err()); + } + +} diff --git a/rust/src/applayerpgsql/pgsql.rs b/rust/src/applayerpgsql/pgsql.rs new file mode 100644 index 000000000000..499ec04cda80 --- /dev/null +++ b/rust/src/applayerpgsql/pgsql.rs @@ -0,0 +1,696 @@ +/* Copyright (C) 2018-2020 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. + */ + +use std; +use crate::core::{self, ALPROTO_UNKNOWN, AppProto, Flow, IPPROTO_TCP}; +use std::mem::transmute; +use crate::applayer::{self, *}; +use std::ffi::CString; +use nom; +use parser::{PgsqlGenericStartupMessage, PgsqlSslResponse}; +use std::fmt; +use super::parser; +use nom::error::{ErrorKind, ParseError}; + +static mut ALPROTO_PGSQL: AppProto = ALPROTO_UNKNOWN; + +#[derive(Debug)] +pub enum PGSQLError { + MalformedData, + NotEnoughData, // not sure about this + StreamTooSmall, // not sure about this + UnknownError, //TODO delete! + NomError(ErrorKind), +} + +impl ParseError for PGSQLError { + fn from_error_kind(_input: I, kind: ErrorKind) -> Self { + PGSQLError::NomError(kind) + } + + fn append(_input: I, kind: ErrorKind, _other: Self) -> Self { + PGSQLError::NomError(kind) + } +} + +impl fmt::Display for PGSQLError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + PGSQLError::MalformedData => f.write_str("Malformed data"), + PGSQLError::NotEnoughData => f.write_str("Not enough data"), + PGSQLError::StreamTooSmall => f.write_str("Stream is too small"), + PGSQLError::UnknownError => f.write_str("Unknown error"), + PGSQLError::NomError(ref inner) => f.write_str(inner.description()), + } + } +} + +pub struct PGSQLTransaction { + tx_id: u64, + pub request: Option, + pub response: Option, + + de_state: Option<*mut core::DetectEngineState>, + events: *mut core::AppLayerDecoderEvents, + tx_data: AppLayerTxData, +} + +impl PGSQLTransaction { + pub fn new() -> PGSQLTransaction { + PGSQLTransaction { + tx_id: 0, + request: None, + response: None, + de_state: None, + events: std::ptr::null_mut(), + tx_data: AppLayerTxData::new(), + } + } + + pub fn free(&mut self) { + if self.events != std::ptr::null_mut() { + core::sc_app_layer_decoder_events_free_events(&mut self.events); + } + if let Some(state) = self.de_state { + core::sc_detect_engine_state_free(state); + } + } +} + +impl Drop for PGSQLTransaction { + fn drop(&mut self) { + self.free(); + } +} + +pub struct PGSQLState { + tx_id: u64, + transactions: Vec, + request_gap: bool, + response_gap: bool, +} + +impl PGSQLState { + pub fn new() -> Self { + Self { + tx_id: 0, + transactions: Vec::new(), + request_gap: false, + response_gap: false, + } + } + + // Free a transaction by ID. + fn free_tx(&mut self, tx_id: u64) { + let len = self.transactions.len(); + let mut found = false; + let mut index = 0; + for i in 0..len { + let tx = &self.transactions[i]; + if tx.tx_id == tx_id + 1 { + found = true; + index = i; + break; + } + } + if found { + self.transactions.remove(index); + } + } + + pub fn get_tx(&mut self, tx_id: u64) -> Option<&PGSQLTransaction> { + for tx in &mut self.transactions { + if tx.tx_id == tx_id + 1 { + return Some(tx); + } + } + return None; + } + + fn new_tx(&mut self) -> PGSQLTransaction { + let mut tx = PGSQLTransaction::new(); + self.tx_id += 1; + tx.tx_id = self.tx_id; + return tx; + } + + fn find_request(&mut self) -> Option<&mut PGSQLTransaction> { + for tx in &mut self.transactions { + if tx.response.is_none() { + return Some(tx); + } + } + None + } + + fn parse_request(&mut self, input: &[u8]) -> AppLayerResult { + // We're not interested in empty requests. + if input.len() == 0 { + return AppLayerResult::ok(); + } + + // If there was gap, check we can sync up again. + if self.request_gap { + if probe_request(input).is_err() { + // The parser now needs to decide what to do as we are not in sync. + // For this pgsql, 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.len() > 0 { + match parser::parse_message(start) { + Ok((rem, request)) => { + start = rem; + + SCLogNotice!("Request: {}", request); + let mut tx = self.new_tx(); + tx.request = Some(request); + self.transactions.push(tx); + }, + Err(nom::Err::Incomplete(_)) => { + // 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 the + // parse is called as soon as more data is received. + 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]) -> AppLayerResult { + // We're not interested in empty responses. + if input.len() == 0 { + return AppLayerResult::ok(); + } + + let mut start = input; + while start.len() > 0 { + match parser::parse_message(start) { + Ok((rem, response)) => { + start = rem; + + match self.find_request() { + Some(tx) => { + tx.response = Some(response); + SCLogNotice!("Found response for request:"); + SCLogNotice!("- Request: {:?}", tx.request); + SCLogNotice!("- Response: {:?}", tx.response); + } + None => {} + } + } + Err(nom::Err::Incomplete(_)) => { + 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 tx_iterator( + &mut self, + min_tx_id: u64, + state: &mut u64, + ) -> Option<(&PGSQLTransaction, u64, bool)> { + let mut index = *state as usize; + let len = self.transactions.len(); + + while index < len { + let tx = &self.transactions[index]; + if tx.tx_id < min_tx_id + 1 { + index += 1; + continue; + } + *state = index as u64; + return Some((tx, tx.tx_id - 1, (len - index) > 1)); + } + + return None; + } + + fn on_request_gap(&mut self, _size: u32) { + self.request_gap = true; + } + + fn on_response_gap(&mut self, _size: u32) { + self.response_gap = true; + } +} + +/// Probe for a valid header. +/// +/// As this pgsql protocol uses messages prefixed with the size +/// as a string followed by a ':', we look at up to the first 10 +/// characters for that pattern. +fn probe_request(input: &[u8]) -> nom::IResult<&[u8], PgsqlGenericStartupMessage> { + // let size = std::cmp::min(10, input.len()); + // let (rem, prefix) = nom::bytes::complete::take(size)(input)?; + // nom::sequence::terminated( + // nom::bytes::complete::take_while1(nom::character::is_digit), + // nom::bytes::complete::tag(":"), + // )(prefix)?; + // Ok((rem, ())) + parser::parse_ssl_request(&input) +} + +fn probe_response(input: &[u8]) -> nom::IResult<&[u8], PgsqlSslResponse> { + // let size = std::cmp::min(10, input.len()); + // let (rem, prefix) = nom::bytes::complete::take(size)(input)?; + // nom::sequence::terminated( + // nom::bytes::complete::take_while1(nom::character::is_digit), + // nom::bytes::complete::tag(":"), + // )(prefix)?; + // Ok((rem, ())) + parser::parse_ssl_response(&input) +} + +// C exports. + +export_tx_get_detect_state!( + rs_pgsql_tx_get_detect_state, + PGSQLTransaction +); +export_tx_set_detect_state!( + rs_pgsql_tx_set_detect_state, + PGSQLTransaction +); + +/// C entry point for a probing parser. +#[no_mangle] +pub extern "C" fn rs_pgsql_probing_parser_ts( + _flow: *const Flow, + _direction: u8, + input: *const u8, + input_len: u32, + _rdir: *mut u8 +) -> AppProto { + // Need at least 2 bytes. + if input_len > 1 && input != std::ptr::null_mut() { + let slice = build_slice!(input, input_len as usize); + if probe_request(slice).is_ok() { + return unsafe { ALPROTO_PGSQL }; + } + } + return ALPROTO_UNKNOWN; +} + +#[no_mangle] +pub extern "C" fn rs_pgsql_probing_parser_tc( + _flow: *const Flow, + _direction: u8, + input: *const u8, + input_len: u32, + _rdir: *mut u8 +) -> AppProto { + // Need at least 2 bytes. + if input_len > 1 && input != std::ptr::null_mut() { + let slice = build_slice!(input, input_len as usize); + if probe_response(slice).is_ok() { + return unsafe { ALPROTO_PGSQL }; + } + } + return ALPROTO_UNKNOWN; +} + +#[no_mangle] +pub extern "C" fn rs_pgsql_state_new(_orig_state: *mut std::os::raw::c_void, _orig_proto: AppProto) -> *mut std::os::raw::c_void { + let state = PGSQLState::new(); + let boxed = Box::new(state); + return unsafe { transmute(boxed) }; +} + +#[no_mangle] +pub extern "C" fn rs_pgsql_state_free(state: *mut std::os::raw::c_void) { + // Just unbox... + let _drop: Box = unsafe { transmute(state) }; +} + +#[no_mangle] +pub extern "C" fn rs_pgsql_state_tx_free( + state: *mut std::os::raw::c_void, + tx_id: u64, +) { + let state = cast_pointer!(state, PGSQLState); + state.free_tx(tx_id); +} + +#[no_mangle] +pub extern "C" fn rs_pgsql_parse_request( + _flow: *const Flow, + state: *mut std::os::raw::c_void, + pstate: *mut std::os::raw::c_void, + input: *const u8, + input_len: u32, + _data: *const std::os::raw::c_void, + _flags: u8, +) -> AppLayerResult { + let eof = unsafe { + if AppLayerParserStateIssetFlag(pstate, APP_LAYER_PARSER_EOF) > 0 { + true + } else { + false + } + }; + + if eof { + // If needed, handled EOF, or pass it into the parser. + return AppLayerResult::ok(); + } + + let state = cast_pointer!(state, PGSQLState); + + if input == std::ptr::null_mut() && input_len > 0 { + // 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(input_len); + AppLayerResult::ok() + } else { + let buf = build_slice!(input, input_len as usize); + state.parse_request(buf) + } +} + +#[no_mangle] +pub extern "C" fn rs_pgsql_parse_response( + _flow: *const Flow, + state: *mut std::os::raw::c_void, + pstate: *mut std::os::raw::c_void, + input: *const u8, + input_len: u32, + _data: *const std::os::raw::c_void, + _flags: u8, +) -> AppLayerResult { + let _eof = unsafe { + if AppLayerParserStateIssetFlag(pstate, APP_LAYER_PARSER_EOF) > 0 { + true + } else { + false + } + }; + let state = cast_pointer!(state, PGSQLState); + + if input == std::ptr::null_mut() && input_len > 0 { + // 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(input_len); + AppLayerResult::ok() + } else { + let buf = build_slice!(input, input_len as usize); + state.parse_response(buf).into() + } +} + +#[no_mangle] +pub extern "C" fn rs_pgsql_state_get_tx( + state: *mut std::os::raw::c_void, + tx_id: u64, +) -> *mut std::os::raw::c_void { + let state = cast_pointer!(state, PGSQLState); + match state.get_tx(tx_id) { + Some(tx) => { + return unsafe { transmute(tx) }; + } + None => { + return std::ptr::null_mut(); + } + } +} + +#[no_mangle] +pub extern "C" fn rs_pgsql_state_get_tx_count( + state: *mut std::os::raw::c_void, +) -> u64 { + let state = cast_pointer!(state, PGSQLState); + return state.tx_id; +} + +#[no_mangle] +pub extern "C" fn rs_pgsql_tx_get_alstate_progress( + tx: *mut std::os::raw::c_void, + _direction: u8, +) -> std::os::raw::c_int { + let tx = cast_pointer!(tx, PGSQLTransaction); + + // Transaction is done if we have a response. + if tx.response.is_some() { + return 1; + } + return 0; +} + +#[no_mangle] +pub extern "C" fn rs_pgsql_state_get_events( + tx: *mut std::os::raw::c_void +) -> *mut core::AppLayerDecoderEvents { + let tx = cast_pointer!(tx, PGSQLTransaction); + return tx.events; +} + +#[no_mangle] +pub extern "C" fn rs_pgsql_state_get_event_info( + _event_name: *const std::os::raw::c_char, + _event_id: *mut std::os::raw::c_int, + _event_type: *mut core::AppLayerEventType, +) -> std::os::raw::c_int { + return -1; +} + +#[no_mangle] +pub extern "C" fn rs_pgsql_state_get_event_info_by_id(_event_id: std::os::raw::c_int, + _event_name: *mut *const std::os::raw::c_char, + _event_type: *mut core::AppLayerEventType +) -> i8 { + return -1; +} +#[no_mangle] +pub extern "C" fn rs_pgsql_state_get_tx_iterator( + _ipproto: u8, + _alproto: AppProto, + state: *mut std::os::raw::c_void, + min_tx_id: u64, + _max_tx_id: u64, + istate: &mut u64, +) -> applayer::AppLayerGetTxIterTuple { + let state = cast_pointer!(state, PGSQLState); + match state.tx_iterator(min_tx_id, istate) { + Some((tx, out_tx_id, has_next)) => { + let c_tx = unsafe { transmute(tx) }; + let ires = applayer::AppLayerGetTxIterTuple::with_values( + c_tx, + out_tx_id, + has_next, + ); + return ires; + } + None => { + return applayer::AppLayerGetTxIterTuple::not_found(); + } + } +} + +/// Get the request buffer for a transaction from C. +/// +/// No required for parsing, but an example function for retrieving a +/// pointer to the request buffer from C for detection. +#[no_mangle] +pub extern "C" fn rs_pgsql_get_request_buffer( + tx: *mut std::os::raw::c_void, + buf: *mut *const u8, + len: *mut u32, +) -> u8 +{ + let tx = cast_pointer!(tx, PGSQLTransaction); + if let Some(ref request) = tx.request { + if request.len() > 0 { + unsafe { + *len = request.len() as u32; + *buf = request.as_ptr(); + } + return 1; + } + } + return 0; +} + +/// Get the response buffer for a transaction from C. +#[no_mangle] +pub extern "C" fn rs_pgsql_get_response_buffer( + tx: *mut std::os::raw::c_void, + buf: *mut *const u8, + len: *mut u32, +) -> u8 +{ + let tx = cast_pointer!(tx, PGSQLTransaction); + if let Some(ref response) = tx.response { + if response.len() > 0 { + unsafe { + *len = response.len() as u32; + *buf = response.as_ptr(); + } + return 1; + } + } + return 0; +} + +export_tx_data_get!(rs_pgsql_get_tx_data, PGSQLTransaction); + +// Parser name as a C style string. +const PARSER_NAME: &'static [u8] = b"pgsql\0"; + +#[no_mangle] +pub unsafe extern "C" fn rs_pgsql_register_parser() { + let default_port = CString::new("[5432]").unwrap(); + let parser = RustParser { + name: PARSER_NAME.as_ptr() as *const std::os::raw::c_char, + default_port: default_port.as_ptr(), + ipproto: IPPROTO_TCP, + probe_ts: Some(rs_pgsql_probing_parser_ts), + probe_tc: Some(rs_pgsql_probing_parser_tc), + min_depth: 0, + max_depth: 16, + state_new: rs_pgsql_state_new, + state_free: rs_pgsql_state_free, + tx_free: rs_pgsql_state_tx_free, + parse_ts: rs_pgsql_parse_request, + parse_tc: rs_pgsql_parse_response, + get_tx_count: rs_pgsql_state_get_tx_count, + get_tx: rs_pgsql_state_get_tx, + tx_comp_st_ts: 1, + tx_comp_st_tc: 1, + tx_get_progress: rs_pgsql_tx_get_alstate_progress, + get_de_state: rs_pgsql_tx_get_detect_state, + set_de_state: rs_pgsql_tx_set_detect_state, + get_events: Some(rs_pgsql_state_get_events), + get_eventinfo: Some(rs_pgsql_state_get_event_info), + get_eventinfo_byid : Some(rs_pgsql_state_get_event_info_by_id), + localstorage_new: None, + localstorage_free: None, + get_files: None, + get_tx_iterator: Some(rs_pgsql_state_get_tx_iterator), + get_tx_data: rs_pgsql_get_tx_data, + apply_tx_config: None, + flags: APP_LAYER_PARSER_OPT_ACCEPT_GAPS, + truncate: 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_PGSQL = alproto; + if AppLayerParserConfParserEnabled( + ip_proto_str.as_ptr(), + parser.name, + ) != 0 + { + let _ = AppLayerRegisterParser(&parser, alproto); + } + SCLogNotice!("Rust pgsql parser registered."); + } else { + SCLogNotice!("Protocol detector and parser disabled for PGSQL."); + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_probe_ssl_request() { + + let buf: &[u8] = &[0x00, 0x00, 0x00, 0x08, 0x04, 0xd2, 0x16, 0x2f]; + let res = probe_request(&buf); + assert!(res.is_ok()); + + let buf: &[u8] = &[0x00, 0x00, 0x00, 0x08, 0x04, 0xd2, 0x16, 0x30]; + let res = probe_request(&buf); + assert!(res.is_err()); + + let buf: &[u8] = &[0x00, 0x00, 0x00, 0x08, 0x04, 0xd2]; + let res = probe_request(&buf); + assert!(res.is_err()); + println!("{}", res.unwrap_err()); + } + + #[test] + fn test_probe_ssl_response() { + let buf: &[u8] = &[0x53]; + let res = probe_response(&buf); + assert!(res.is_ok()); + + let buf: &[u8] = &[0x4e]; + let res = probe_response(&buf); + assert!(res.is_ok()); + + let buf: &[u8] = &[0x30]; + let res = probe_response(&buf); + assert!(res.is_err()); + + let buf: &[u8] = &[0x30, 0x31]; + let res = probe_response(&buf); + assert!(res.is_err()); + } + + #[test] + fn test_incomplete() { + let mut state = PGSQLState::new(); + let buf = b"5:Hello3:bye"; + + let r = state.parse_request(&buf[0..0]); + assert_eq!(r, AppLayerResult{ status: 0, consumed: 0, needed: 0}); + + let r = state.parse_request(&buf[0..1]); + assert_eq!(r, AppLayerResult{ status: 1, consumed: 0, needed: 2}); + + let r = state.parse_request(&buf[0..2]); + assert_eq!(r, AppLayerResult{ status: 1, consumed: 0, needed: 3}); + + // This is the first message and only the first message. + let r = state.parse_request(&buf[0..7]); + assert_eq!(r, AppLayerResult{ status: 0, consumed: 0, needed: 0}); + + // The first message and a portion of the second. + let r = state.parse_request(&buf[0..9]); + assert_eq!(r, AppLayerResult{ status: 1, consumed: 7, needed: 3}); + } +} diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 4d054abad1d3..e1275be88218 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -76,6 +76,7 @@ pub mod dhcp; pub mod sip; pub mod rfb; pub mod mqtt; +pub mod applayerpgsql; pub mod applayertemplate; pub mod rdp; pub mod x509; diff --git a/src/Makefile.am b/src/Makefile.am index 137f3101082e..3f4d7ce0b3cf 100755 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -51,6 +51,7 @@ app-layer-ikev2.c app-layer-ikev2.h \ app-layer-krb5.c app-layer-krb5.h \ app-layer-rfb.c app-layer-rfb.h \ app-layer-mqtt.c app-layer-mqtt.h \ +app-layer-pgsql.c app-layer-pgsql.h \ app-layer-template.c app-layer-template.h \ app-layer-template-rust.c app-layer-template-rust.h \ app-layer-rdp.c app-layer-rdp.h \ @@ -393,6 +394,7 @@ output-json-dhcp.c output-json-dhcp.h \ output-json-snmp.c output-json-snmp.h \ output-json-rfb.c output-json-rfb.h \ output-json-mqtt.c output-json-mqtt.h \ +output-json-pgsql.c output-json-pgsql.h \ output-json-template.c output-json-template.h \ output-json-template-rust.c output-json-template-rust.h \ output-json-rdp.c output-json-rdp.h \ diff --git a/src/app-layer-detect-proto.c b/src/app-layer-detect-proto.c index c123e12e3f58..1fe89db97ec4 100644 --- a/src/app-layer-detect-proto.c +++ b/src/app-layer-detect-proto.c @@ -915,6 +915,8 @@ static void AppLayerProtoDetectPrintProbingParsers(AppLayerProtoDetectProbingPar printf(" alproto: ALPROTO_RFB\n"); else if (pp_pe->alproto == ALPROTO_MQTT) printf(" alproto: ALPROTO_MQTT\n"); + else if (pp_pe->alproto == ALPROTO_PGSQL) + printf(" alproto: ALPROTO_PGSQL\n"); else if (pp_pe->alproto == ALPROTO_TEMPLATE) printf(" alproto: ALPROTO_TEMPLATE\n"); else if (pp_pe->alproto == ALPROTO_DNP3) @@ -992,6 +994,8 @@ static void AppLayerProtoDetectPrintProbingParsers(AppLayerProtoDetectProbingPar printf(" alproto: ALPROTO_RFB\n"); else if (pp_pe->alproto == ALPROTO_MQTT) printf(" alproto: ALPROTO_MQTT\n"); + else if (pp_pe->alproto == ALPROTO_PGSQL) + printf(" alproto: ALPROTO_PGSQL\n"); else if (pp_pe->alproto == ALPROTO_TEMPLATE) printf(" alproto: ALPROTO_TEMPLATE\n"); else if (pp_pe->alproto == ALPROTO_DNP3) diff --git a/src/app-layer-parser.c b/src/app-layer-parser.c index f94c81ddebb1..2af4e3ac3a97 100644 --- a/src/app-layer-parser.c +++ b/src/app-layer-parser.c @@ -67,6 +67,7 @@ #include "app-layer-sip.h" #include "app-layer-rfb.h" #include "app-layer-mqtt.h" +#include "app-layer-pgsql.h" #include "app-layer-template.h" #include "app-layer-template-rust.h" #include "app-layer-rdp.h" @@ -1629,6 +1630,7 @@ void AppLayerParserRegisterProtocolParsers(void) RegisterTemplateRustParsers(); RegisterRFBParsers(); RegisterMQTTParsers(); + RegisterPGSQLParsers(); RegisterTemplateParsers(); RegisterRdpParsers(); RegisterHTTP2Parsers(); diff --git a/src/app-layer-pgsql.c b/src/app-layer-pgsql.c new file mode 100644 index 000000000000..db5c35952cee --- /dev/null +++ b/src/app-layer-pgsql.c @@ -0,0 +1,64 @@ +/* Copyright (C) 2018 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. + */ + +/* + * TODO: Update \author in this file and app-layer-pgsqlrust.h. + * TODO: Implement your app-layer logic with unit tests. + * TODO: Remove SCLogNotice statements or convert to debug. + */ + +/** + * \file + * + * \author FirstName LastName + * + * PGSQL application layer detector and parser for learning and + * pgsqlrust pruposes. + * + * This pgsqlrust implements a simple application layer for something + * like the echo protocol running on port 7. + */ + +#include "suricata-common.h" +#include "stream.h" +#include "conf.h" + +#include "util-unittest.h" + +#include "app-layer-detect-proto.h" +#include "app-layer-parser.h" + +#include "app-layer-pgsql.h" +#include "rust.h" + +void RegisterPGSQLParsers(void) +{ + SCLogNotice("Registering Rust pgsql parser."); + rs_pgsql_register_parser(); +#ifdef UNITTESTS + AppLayerParserRegisterProtocolUnittests(IPPROTO_TCP, ALPROTO_PGSQL, PGSQLParserRegisterTests); +#endif +} + +#ifdef UNITTESTS +#endif + +void PGSQLParserRegisterTests(void) +{ +#ifdef UNITTESTS +#endif +} diff --git a/src/app-layer-pgsql.h b/src/app-layer-pgsql.h new file mode 100644 index 000000000000..2dcb2c99a2b4 --- /dev/null +++ b/src/app-layer-pgsql.h @@ -0,0 +1,30 @@ +/* Copyright (C) 2018 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. + */ + +/** + * \file + * + * \author FirstName LastName + */ + +#ifndef __APP_LAYER_PGSQL_H__ +#define __APP_LAYER_PGSQL_H__ + +void RegisterPGSQLParsers(void); +void PGSQLParserRegisterTests(void); + +#endif /* __APP_LAYER_PGSQL_H__ */ diff --git a/src/app-layer-protos.c b/src/app-layer-protos.c index ecb00684ae1d..83be15a81ae3 100644 --- a/src/app-layer-protos.c +++ b/src/app-layer-protos.c @@ -108,6 +108,9 @@ const char *AppProtoToString(AppProto alproto) case ALPROTO_MQTT: proto_name = "mqtt"; break; + case ALPROTO_PGSQL: + proto_name = "pgsql"; + break; case ALPROTO_TEMPLATE: proto_name = "template"; break; @@ -163,6 +166,8 @@ AppProto StringToAppProto(const char *proto_name) if (strcmp(proto_name,"sip")==0) return ALPROTO_SIP; if (strcmp(proto_name,"rfb")==0) return ALPROTO_RFB; if (strcmp(proto_name,"mqtt")==0) return ALPROTO_MQTT; + if (strcmp(proto_name, "pgsql") == 0) + return ALPROTO_PGSQL; if (strcmp(proto_name,"template")==0) return ALPROTO_TEMPLATE; if (strcmp(proto_name,"template-rust")==0) return ALPROTO_TEMPLATE_RUST; if (strcmp(proto_name,"rdp")==0) return ALPROTO_RDP; diff --git a/src/app-layer-protos.h b/src/app-layer-protos.h index 278c3e4389b0..86977afc5827 100644 --- a/src/app-layer-protos.h +++ b/src/app-layer-protos.h @@ -53,6 +53,7 @@ enum AppProtoEnum { ALPROTO_SIP, ALPROTO_RFB, ALPROTO_MQTT, + ALPROTO_PGSQL, ALPROTO_TEMPLATE, ALPROTO_TEMPLATE_RUST, ALPROTO_RDP, diff --git a/src/output-json-pgsql.c b/src/output-json-pgsql.c new file mode 100644 index 000000000000..14e6bcd4739e --- /dev/null +++ b/src/output-json-pgsql.c @@ -0,0 +1,186 @@ +/* Copyright (C) 2018-2020 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. + */ + +/* + * TODO: Update \author in this file and in output-json-pgsql.h. + * TODO: Remove SCLogNotice statements, or convert to debug. + * TODO: Implement your app-layers logging. + */ + +/** + * \file + * + * \author FirstName LastName + * + * Implement JSON/eve logging app-layer PGSQL. + */ + +#include "suricata-common.h" +#include "debug.h" +#include "detect.h" +#include "pkt-var.h" +#include "conf.h" + +#include "threads.h" +#include "threadvars.h" +#include "tm-threads.h" + +#include "util-unittest.h" +#include "util-buffer.h" +#include "util-debug.h" +#include "util-byte.h" + +#include "output.h" +#include "output-json.h" + +#include "app-layer.h" +#include "app-layer-parser.h" + +#include "app-layer-pgsql.h" +#include "output-json-pgsql.h" +#include "rust.h" + +typedef struct LogPGSQLFileCtx_ { + LogFileCtx *file_ctx; + uint32_t flags; +} LogPGSQLFileCtx; + +typedef struct LogPGSQLLogThread_ { + LogPGSQLFileCtx *pgsqllog_ctx; + LogFileCtx *file_ctx; + MemBuffer *buffer; +} LogPGSQLLogThread; + +static int JsonPGSQLLogger(ThreadVars *tv, void *thread_data, const Packet *p, Flow *f, void *state, + void *tx, uint64_t tx_id) +{ + SCLogNotice("JsonPGSQLLogger"); + LogPGSQLLogThread *thread = thread_data; + + JsonBuilder *js = CreateEveHeader(p, LOG_DIR_PACKET, "pgsql", NULL); + if (unlikely(js == NULL)) { + return TM_ECODE_FAILED; + } + + jb_open_object(js, "pgsql"); + if (!rs_pgsql_logger_log(tx, js)) { + goto error; + } + jb_close(js); + + MemBufferReset(thread->buffer); + OutputJsonBuilderBuffer(js, thread->file_ctx, &thread->buffer); + jb_free(js); + + return TM_ECODE_OK; + +error: + jb_free(js); + return TM_ECODE_FAILED; +} + +static void OutputPGSQLLogDeInitCtxSub(OutputCtx *output_ctx) +{ + LogPGSQLFileCtx *pgsqllog_ctx = (LogPGSQLFileCtx *)output_ctx->data; + SCFree(pgsqllog_ctx); + SCFree(output_ctx); +} + +static OutputInitResult OutputPGSQLLogInitSub(ConfNode *conf, OutputCtx *parent_ctx) +{ + OutputInitResult result = { NULL, false }; + OutputJsonCtx *ajt = parent_ctx->data; + + LogPGSQLFileCtx *pgsqllog_ctx = SCCalloc(1, sizeof(*pgsqllog_ctx)); + if (unlikely(pgsqllog_ctx == NULL)) { + return result; + } + pgsqllog_ctx->file_ctx = ajt->file_ctx; + + OutputCtx *output_ctx = SCCalloc(1, sizeof(*output_ctx)); + if (unlikely(output_ctx == NULL)) { + SCFree(pgsqllog_ctx); + return result; + } + output_ctx->data = pgsqllog_ctx; + output_ctx->DeInit = OutputPGSQLLogDeInitCtxSub; + + SCLogNotice("PGSQL log sub-module initialized."); + + AppLayerParserRegisterLogger(IPPROTO_TCP, ALPROTO_PGSQL); + + result.ctx = output_ctx; + result.ok = true; + return result; +} + +static TmEcode JsonPGSQLLogThreadInit(ThreadVars *t, const void *initdata, void **data) +{ + LogPGSQLLogThread *thread = SCCalloc(1, sizeof(*thread)); + if (unlikely(thread == NULL)) { + return TM_ECODE_FAILED; + } + + if (initdata == NULL) { + SCLogDebug("Error getting context for EveLogPGSQL. \"initdata\" is NULL."); + goto error_exit; + } + + thread->buffer = MemBufferCreateNew(JSON_OUTPUT_BUFFER_SIZE); + if (unlikely(thread->buffer == NULL)) { + goto error_exit; + } + + thread->pgsqllog_ctx = ((OutputCtx *)initdata)->data; + thread->file_ctx = LogFileEnsureExists(thread->pgsqllog_ctx->file_ctx, t->id); + if (!thread->file_ctx) { + goto error_exit; + } + *data = (void *)thread; + + return TM_ECODE_OK; + +error_exit: + if (thread->buffer != NULL) { + MemBufferFree(thread->buffer); + } + SCFree(thread); + return TM_ECODE_FAILED; +} + +static TmEcode JsonPGSQLLogThreadDeinit(ThreadVars *t, void *data) +{ + LogPGSQLLogThread *thread = (LogPGSQLLogThread *)data; + if (thread == NULL) { + return TM_ECODE_OK; + } + if (thread->buffer != NULL) { + MemBufferFree(thread->buffer); + } + SCFree(thread); + return TM_ECODE_OK; +} + +void JsonPGSQLLogRegister(void) +{ + /* Register as an eve sub-module. */ + OutputRegisterTxSubModule(LOGGER_JSON_PGSQL, "eve-log", "JsonPGSQLLog", "eve-log.pgsql", + OutputPGSQLLogInitSub, ALPROTO_PGSQL, JsonPGSQLLogger, JsonPGSQLLogThreadInit, + JsonPGSQLLogThreadDeinit, NULL); + + SCLogNotice("PGSQL JSON logger registered."); +} diff --git a/src/output-json-pgsql.h b/src/output-json-pgsql.h new file mode 100644 index 000000000000..c1643d1436ac --- /dev/null +++ b/src/output-json-pgsql.h @@ -0,0 +1,29 @@ +/* Copyright (C) 2018 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. + */ + +/** + * \file + * + * \author FirstName LastName + */ + +#ifndef __OUTPUT_JSON_PGSQL_H__ +#define __OUTPUT_JSON_PGSQL_H__ + +void JsonPGSQLLogRegister(void); + +#endif /* __OUTPUT_JSON_PGSQL_H__ */ diff --git a/src/output.c b/src/output.c index ca6f53850582..cfd9a9c0bd6f 100644 --- a/src/output.c +++ b/src/output.c @@ -76,6 +76,7 @@ #include "output-json-sip.h" #include "output-json-rfb.h" #include "output-json-mqtt.h" +#include "output-json-pgsql.h" #include "output-json-template.h" #include "output-json-template-rust.h" #include "output-json-rdp.h" @@ -1110,6 +1111,8 @@ void OutputRegisterLoggers(void) JsonRFBLogRegister(); /* MQTT JSON logger. */ JsonMQTTLogRegister(); + /* PGSQL JSON logger. */ + JsonPGSQLLogRegister(); /* Template JSON logger. */ JsonTemplateLogRegister(); /* Template Rust JSON logger. */ diff --git a/src/suricata-common.h b/src/suricata-common.h index 160cde8173a3..87a709f79cac 100644 --- a/src/suricata-common.h +++ b/src/suricata-common.h @@ -462,6 +462,7 @@ typedef enum { LOGGER_JSON_TEMPLATE_RUST, LOGGER_JSON_RFB, LOGGER_JSON_MQTT, + LOGGER_JSON_PGSQL, LOGGER_JSON_TEMPLATE, LOGGER_JSON_RDP, LOGGER_JSON_DCERPC, diff --git a/src/util-profiling.c b/src/util-profiling.c index cc6d5c0856e8..339abf9e64ac 100644 --- a/src/util-profiling.c +++ b/src/util-profiling.c @@ -1319,6 +1319,7 @@ const char * PacketProfileLoggertIdToString(LoggerId id) CASE_CODE (LOGGER_JSON_TEMPLATE_RUST); CASE_CODE (LOGGER_JSON_RFB); CASE_CODE (LOGGER_JSON_MQTT); + CASE_CODE(LOGGER_JSON_PGSQL); CASE_CODE (LOGGER_JSON_TEMPLATE); CASE_CODE (LOGGER_JSON_RDP); CASE_CODE (LOGGER_JSON_DCERPC); diff --git a/suricata.yaml.in b/suricata.yaml.in index 3f1bae8f11fd..01e079cc4072 100644 --- a/suricata.yaml.in +++ b/suricata.yaml.in @@ -149,6 +149,7 @@ outputs: header: X-Forwarded-For types: + - pgsql - alert: # payload: yes # enable dumping payload in Base64 # payload-buffer-size: 4kb # max size of payload buffer to output in eve-log @@ -714,6 +715,8 @@ pcap-file: # "detection-only" enables protocol detection only (parser disabled). app-layer: protocols: + pgsql: + enabled: yes rfb: enabled: yes detection-ports: