/* Copyright (C) 2022 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: Juliana Fajardini //! PostgreSQL parser json logger use crate::jsonbuilder::{JsonBuilder, JsonError}; use crate::pgsql::parser::*; use crate::pgsql::pgsql::*; use std; pub const PGSQL_LOG_PASSWORDS: u32 = BIT_U32!(0); fn log_pgsql(tx: &PgsqlTransaction, flags: u32, js: &mut JsonBuilder) -> Result<(), JsonError> { js.open_object("pgsql")?; js.set_uint("tx_id", tx.tx_id)?; if let Some(request) = &tx.request { js.set_object("request", &log_request(request, flags)?)?; } else if tx.responses.is_empty() { SCLogDebug!("Suricata created an empty PGSQL transaction"); // TODO Log anomaly event? // if there are no transactions, there's nothing more to be logged js.close()?; return Ok(()); } if !tx.responses.is_empty() { SCLogDebug!("Responses length: {}", tx.responses.len()); js.set_object("response", &log_response_object(tx)?)?; } js.close()?; Ok(()) } fn log_request(req: &PgsqlFEMessage, flags: u32) -> Result { let mut js = JsonBuilder::try_new_object()?; match req { PgsqlFEMessage::StartupMessage(StartupPacket { length: _, proto_major, proto_minor, params, }) => { let proto = format!("{}.{}", proto_major, proto_minor); js.set_string("protocol_version", &proto)?; js.set_object("startup_parameters", &log_startup_parameters(params)?)?; } PgsqlFEMessage::SSLRequest(_) => { js.set_string("message", "SSL Request")?; } PgsqlFEMessage::SASLInitialResponse(SASLInitialResponsePacket { identifier: _, length: _, auth_mechanism, param_length: _, sasl_param, }) => { js.set_string("sasl_authentication_mechanism", auth_mechanism.to_str())?; js.set_string_from_bytes("sasl_param", sasl_param)?; } PgsqlFEMessage::PasswordMessage(RegularPacket { identifier: _, length: _, payload, }) => { if flags & PGSQL_LOG_PASSWORDS != 0 { js.set_string_from_bytes("password", payload)?; } } PgsqlFEMessage::SASLResponse(RegularPacket { identifier: _, length: _, payload, }) => { js.set_string_from_bytes("sasl_response", payload)?; } PgsqlFEMessage::SimpleQuery(RegularPacket { identifier: _, length: _, payload, }) => { js.set_string_from_bytes(req.to_str(), payload)?; } PgsqlFEMessage::CancelRequest(CancelRequestMessage { pid, backend_key }) => { js.set_string("message", "cancel_request")?; js.set_uint("process_id", *pid)?; js.set_uint("secret_key", *backend_key)?; } PgsqlFEMessage::Terminate(NoPayloadMessage { identifier: _, length: _, }) => { js.set_string("message", req.to_str())?; } PgsqlFEMessage::UnknownMessageType(RegularPacket { identifier: _, length: _, payload: _, }) => { // We don't want to log these, for now. Cf redmine: #6576 } } js.close()?; Ok(js) } fn log_response_object(tx: &PgsqlTransaction) -> Result { let mut jb = JsonBuilder::try_new_object()?; let mut array_open = false; for response in &tx.responses { if let PgsqlBEMessage::ParameterStatus(msg) = response { if !array_open { jb.open_array("parameter_status")?; array_open = true; } jb.append_object(&log_pgsql_param(&msg.param)?)?; } else { if array_open { jb.close()?; array_open = false; } log_response(response, &mut jb)?; } } jb.close()?; Ok(jb) } fn log_response(res: &PgsqlBEMessage, jb: &mut JsonBuilder) -> Result<(), JsonError> { match res { PgsqlBEMessage::SSLResponse(message) => { if let SSLResponseMessage::SSLAccepted = message { jb.set_bool("ssl_accepted", true)?; } else { jb.set_bool("ssl_accepted", false)?; } } PgsqlBEMessage::NoticeResponse(ErrorNoticeMessage { identifier: _, length: _, message_body, }) | PgsqlBEMessage::ErrorResponse(ErrorNoticeMessage { identifier: _, length: _, message_body, }) => { log_error_notice_field_types(message_body, jb)?; } PgsqlBEMessage::AuthenticationMD5Password(AuthenticationMessage { identifier: _, length: _, auth_type: _, payload, }) | PgsqlBEMessage::AuthenticationSSPI(AuthenticationMessage { identifier: _, length: _, auth_type: _, payload, }) | PgsqlBEMessage::AuthenticationSASLFinal(AuthenticationMessage { identifier: _, length: _, auth_type: _, payload, }) | PgsqlBEMessage::CommandComplete(RegularPacket { identifier: _, length: _, payload, }) => { jb.set_string_from_bytes(res.to_str(), payload)?; } PgsqlBEMessage::UnknownMessageType(RegularPacket { identifier: _, length: _, payload: _, }) => { // We don't want to log these, for now. Cf redmine: #6576 } PgsqlBEMessage::AuthenticationOk(_) | PgsqlBEMessage::AuthenticationCleartextPassword(_) | PgsqlBEMessage::AuthenticationSASL(_) | PgsqlBEMessage::AuthenticationSASLContinue(_) | PgsqlBEMessage::CopyDone(_) => { jb.set_string("message", res.to_str())?; } PgsqlBEMessage::ParameterStatus(ParameterStatusMessage { identifier: _, length: _, param: _, }) => { // We take care of these elsewhere } PgsqlBEMessage::CopyOutResponse(CopyOutResponse { identifier: _, length: _, column_cnt, }) => { jb.open_object(res.to_str())?; jb.set_uint("copy_column_count", *column_cnt)?; jb.close()?; } PgsqlBEMessage::BackendKeyData(BackendKeyDataMessage { identifier: _, length: _, backend_pid, secret_key, }) => { jb.set_uint("process_id", *backend_pid)?; jb.set_uint("secret_key", *secret_key)?; } PgsqlBEMessage::ReadyForQuery(ReadyForQueryMessage { identifier: _, length: _, transaction_status: _, }) => { // We don't want to log this one } PgsqlBEMessage::ConsolidatedCopyDataOut(ConsolidatedDataRowPacket { identifier: _, row_cnt, data_size, }) => { jb.open_object(res.to_str())?; jb.set_uint("row_count", *row_cnt)?; jb.set_uint("data_size", *data_size)?; jb.close()?; } PgsqlBEMessage::RowDescription(RowDescriptionMessage { identifier: _, length: _, field_count, fields: _, }) => { jb.set_uint("field_count", *field_count)?; } PgsqlBEMessage::ConsolidatedDataRow(ConsolidatedDataRowPacket { identifier: _, row_cnt, data_size, }) => { jb.set_uint("data_rows", *row_cnt)?; jb.set_uint("data_size", *data_size)?; } PgsqlBEMessage::NotificationResponse(NotificationResponse { identifier: _, length: _, pid, channel_name, payload, }) => { jb.set_uint("pid", *pid)?; jb.set_string_from_bytes("channel_name", channel_name)?; jb.set_string_from_bytes("payload", payload)?; } } Ok(()) } fn log_error_notice_field_types( error_fields: &Vec, jb: &mut JsonBuilder, ) -> Result<(), JsonError> { for field in error_fields { jb.set_string_from_bytes(field.field_type.to_str(), &field.field_value)?; } Ok(()) } fn log_startup_parameters(params: &PgsqlStartupParameters) -> Result { let mut jb = JsonBuilder::try_new_object()?; // User is a mandatory field in a pgsql message jb.set_string_from_bytes("user", ¶ms.user.value)?; if let Some(parameters) = ¶ms.optional_params { jb.open_array("optional_parameters")?; for parameter in parameters { jb.append_object(&log_pgsql_param(parameter)?)?; } jb.close()?; } jb.close()?; Ok(jb) } fn log_pgsql_param(param: &PgsqlParameter) -> Result { let mut jb = JsonBuilder::try_new_object()?; jb.set_string_from_bytes(param.name.to_str(), ¶m.value)?; jb.close()?; Ok(jb) } #[no_mangle] pub unsafe extern "C" fn SCPgsqlLogger( tx: *mut std::os::raw::c_void, flags: u32, js: &mut JsonBuilder, ) -> bool { let tx_pgsql = cast_pointer!(tx, PgsqlTransaction); SCLogDebug!( "----------- PGSQL rs_pgsql_logger call. Tx id is {:?}", tx_pgsql.tx_id ); log_pgsql(tx_pgsql, flags, js).is_ok() }