diff --git a/doc/userguide/rules/index.rst b/doc/userguide/rules/index.rst index e174c6787bc5..6c128748a246 100644 --- a/doc/userguide/rules/index.rst +++ b/doc/userguide/rules/index.rst @@ -35,6 +35,7 @@ Suricata Rules quic-keywords nfs-keywords smtp-keywords + websocket-keywords app-layer xbits thresholding diff --git a/doc/userguide/rules/intro.rst b/doc/userguide/rules/intro.rst index ab35f8a311ca..e2e9da58b610 100644 --- a/doc/userguide/rules/intro.rst +++ b/doc/userguide/rules/intro.rst @@ -110,6 +110,7 @@ you can pick from. These are: * snmp * tftp * sip +* websocket The availability of these protocols depends on whether the protocol is enabled in the configuration file, suricata.yaml. diff --git a/doc/userguide/rules/websocket-keywords.rst b/doc/userguide/rules/websocket-keywords.rst new file mode 100644 index 000000000000..9e5bef2910b1 --- /dev/null +++ b/doc/userguide/rules/websocket-keywords.rst @@ -0,0 +1,50 @@ +WebSocket Keywords +================== + +websocket.payload +----------------- + +A sticky buffer on the unmasked payload, +limited by suricata.yaml config value ``websocket.max-payload-size``. + +Examples:: + + websocket.payload; pcre:"/^123[0-9]*/"; + websocket.payload content:"swordfish"; + +``websocket.payload`` is a 'sticky buffer' and can be used as ``fast_pattern``. + +websocket.fin +------------- + +A boolean to tell if the payload is complete. + +Examples:: + + websocket.fin:true; + websocket.fin:false; + +websocket.mask +-------------- + +Matches on the websocket mask if any. +It uses a 32-bit unsigned integer as value (big-endian). + +Examples:: + + websocket.mask:123456; + websocket.mask:>0; + +websocket.opcode +---------------- + +Matches on the websocket opcode. +It uses a 8-bit unsigned integer as value. +Only 16 values are relevant. +It can also be specified by text from the enumeration + +Examples:: + + websocket.opcode:1; + websocket.opcode:>8; + websocket.opcode:ping; diff --git a/etc/schema.json b/etc/schema.json index c194017ddf6f..f2bc46947271 100644 --- a/etc/schema.json +++ b/etc/schema.json @@ -3823,6 +3823,9 @@ }, "tls": { "$ref": "#/$defs/stats_applayer_error" + }, + "websocket": { + "$ref": "#/$defs/stats_applayer_error" } }, "additionalProperties": false @@ -3940,6 +3943,9 @@ }, "tls": { "type": "integer" + }, + "websocket": { + "type": "integer" } }, "additionalProperties": false @@ -4051,6 +4057,9 @@ }, "tls": { "type": "integer" + }, + "websocket": { + "type": "integer" } }, "additionalProperties": false @@ -5488,7 +5497,23 @@ } }, "additionalProperties": false + }, + "websocket": { + "type": "object", + "properties": { + "fin": { + "type": "boolean" + }, + "mask": { + "type": "integer" + }, + "opcode": { + "type": "string" + } + }, + "additionalProperties": false } + }, "$defs": { "stats_applayer_error": { diff --git a/rust/derive/src/lib.rs b/rust/derive/src/lib.rs index a2b7a6ad0442..8217d0914f01 100644 --- a/rust/derive/src/lib.rs +++ b/rust/derive/src/lib.rs @@ -23,6 +23,7 @@ use proc_macro::TokenStream; mod applayerevent; mod applayerframetype; +mod stringenum; /// The `AppLayerEvent` derive macro generates a `AppLayerEvent` trait /// implementation for enums that define AppLayerEvents. @@ -50,3 +51,8 @@ pub fn derive_app_layer_event(input: TokenStream) -> TokenStream { pub fn derive_app_layer_frame_type(input: TokenStream) -> TokenStream { applayerframetype::derive_app_layer_frame_type(input) } + +#[proc_macro_derive(EnumStringU8, attributes(name))] +pub fn derive_enum_string_u8(input: TokenStream) -> TokenStream { + stringenum::derive_enum_string_u8(input) +} diff --git a/rust/derive/src/stringenum.rs b/rust/derive/src/stringenum.rs new file mode 100644 index 000000000000..ec9dc4cea87a --- /dev/null +++ b/rust/derive/src/stringenum.rs @@ -0,0 +1,76 @@ +/* Copyright (C) 2023 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. + */ + +extern crate proc_macro; +use super::applayerevent::transform_name; +use proc_macro::TokenStream; +use quote::quote; +use syn::{self, parse_macro_input, DeriveInput}; + +pub fn derive_enum_string_u8(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let name = transform_name(&input.ident.to_string()); + let mut values = Vec::new(); + let mut names = Vec::new(); + + if let syn::Data::Enum(ref data) = input.data { + for (_, v) in (&data.variants).into_iter().enumerate() { + let fname = transform_name(&v.ident.to_string()); + names.push(fname); + if let Some((_, val)) = &v.discriminant { + if let syn::Expr::Lit(l) = val { + if let syn::Lit::Int(li) = &l.lit { + if let Ok(value) = li.base10_parse::() { + values.push(value); + } else { + panic!("EnumString requires explicit u8"); + } + } else { + panic!("EnumString requires explicit literal integer"); + } + } else { + panic!("EnumString requires explicit literal"); + } + } else { + panic!("EnumString requires explicit values"); + } + } + } else { + panic!("EnumString can only be derived for enums"); + } + + let stringer = syn::Ident::new(&(name.clone() + "_string"), proc_macro2::Span::call_site()); + let parser = syn::Ident::new(&(name + "_parse"), proc_macro2::Span::call_site()); + + let expanded = quote! { + fn #stringer(v: u8) -> Option<&'static str> { + match v { + #( #values => Some(#names) ,)* + _ => None, + } + } + + pub(crate) fn #parser(v: &str) -> Option { + match v { + #( #names => Some(#values) ,)* + _ => None, + } + } + }; + + proc_macro::TokenStream::from(expanded) +} diff --git a/rust/src/applayer.rs b/rust/src/applayer.rs index 97db321e2249..84f00e910cbf 100644 --- a/rust/src/applayer.rs +++ b/rust/src/applayer.rs @@ -449,6 +449,7 @@ pub unsafe fn AppLayerRegisterParser(parser: *const RustParser, alproto: AppProt // Defined in app-layer-detect-proto.h extern { + pub fn AppLayerRegisterExpectationProto(ipproto: u8, alproto: AppProto); pub fn AppLayerProtoDetectPPRegister(ipproto: u8, portstr: *const c_char, alproto: AppProto, min_depth: u16, max_depth: u16, dir: u8, pparser1: ProbeFn, pparser2: ProbeFn); diff --git a/rust/src/lib.rs b/rust/src/lib.rs index da2859637783..09d7ceb95d4b 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -103,6 +103,7 @@ pub mod rfb; pub mod mqtt; pub mod pgsql; pub mod telnet; +pub mod websocket; pub mod applayertemplate; pub mod rdp; pub mod x509; diff --git a/rust/src/websocket/detect.rs b/rust/src/websocket/detect.rs new file mode 100644 index 000000000000..69925320d5d5 --- /dev/null +++ b/rust/src/websocket/detect.rs @@ -0,0 +1,74 @@ +/* Copyright (C) 2023 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 super::logger::web_socket_opcode_parse; +use super::websocket::WebSocketTransaction; +use crate::detect::uint::{detect_parse_uint, DetectUintData, DetectUintMode}; +use std::ffi::CStr; + +#[no_mangle] +pub unsafe extern "C" fn SCWebSocketGetOpcode(tx: &mut WebSocketTransaction) -> u8 { + return tx.pdu.opcode; +} + +#[no_mangle] +pub unsafe extern "C" fn SCWebSocketGetFin(tx: &mut WebSocketTransaction) -> bool { + return tx.pdu.fin; +} + +#[no_mangle] +pub unsafe extern "C" fn SCWebSocketGetPayload( + tx: &WebSocketTransaction, buffer: *mut *const u8, buffer_len: *mut u32, +) -> bool { + *buffer = tx.pdu.payload.as_ptr(); + *buffer_len = tx.pdu.payload.len() as u32; + return true; +} + +#[no_mangle] +pub unsafe extern "C" fn SCWebSocketGetMask( + tx: &mut WebSocketTransaction, value: *mut u32, +) -> bool { + if let Some(xorkey) = tx.pdu.mask { + *value = xorkey; + return true; + } + return false; +} + +#[no_mangle] +pub unsafe extern "C" fn SCWebSocketParseOpcode( + ustr: *const std::os::raw::c_char, +) -> *mut DetectUintData { + let ft_name: &CStr = CStr::from_ptr(ustr); //unsafe + if let Ok(s) = ft_name.to_str() { + if let Ok((_, ctx)) = detect_parse_uint::(s) { + let boxed = Box::new(ctx); + return Box::into_raw(boxed) as *mut _; + } + if let Some(arg1) = web_socket_opcode_parse(s) { + let ctx = DetectUintData:: { + arg1, + arg2: 0, + mode: DetectUintMode::DetectUintModeEqual, + }; + let boxed = Box::new(ctx); + return Box::into_raw(boxed) as *mut _; + } + } + return std::ptr::null_mut(); +} diff --git a/rust/src/websocket/logger.rs b/rust/src/websocket/logger.rs new file mode 100644 index 000000000000..602ba5ef063e --- /dev/null +++ b/rust/src/websocket/logger.rs @@ -0,0 +1,53 @@ +/* Copyright (C) 2023 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 super::websocket::WebSocketTransaction; +use crate::jsonbuilder::{JsonBuilder, JsonError}; +use std; +use suricata_derive::EnumStringU8; + +#[derive(EnumStringU8)] +pub enum WebSocketOpcode { + Continuation = 0, + Text = 1, + Binary = 2, + Ping = 8, + Pong = 9, +} + +fn log_websocket(tx: &WebSocketTransaction, js: &mut JsonBuilder) -> Result<(), JsonError> { + js.open_object("websocket")?; + js.set_bool("fin", tx.pdu.fin)?; + if let Some(xorkey) = tx.pdu.mask { + js.set_uint("mask", xorkey.into())?; + } + if let Some(val) = web_socket_opcode_string(tx.pdu.opcode) { + js.set_string("opcode", val)?; + } else { + js.set_string("opcode", &format!("unknown-{}", tx.pdu.opcode))?; + } + js.close()?; + Ok(()) +} + +#[no_mangle] +pub unsafe extern "C" fn rs_websocket_logger_log( + tx: *mut std::os::raw::c_void, js: &mut JsonBuilder, +) -> bool { + let tx = cast_pointer!(tx, WebSocketTransaction); + log_websocket(tx, js).is_ok() +} diff --git a/rust/src/websocket/mod.rs b/rust/src/websocket/mod.rs new file mode 100644 index 000000000000..c57660f2a44b --- /dev/null +++ b/rust/src/websocket/mod.rs @@ -0,0 +1,23 @@ +/* Copyright (C) 2023 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 websocket parser and logger module. + +pub mod detect; +pub mod logger; +mod parser; +pub mod websocket; diff --git a/rust/src/websocket/parser.rs b/rust/src/websocket/parser.rs new file mode 100644 index 000000000000..744f9dfcba42 --- /dev/null +++ b/rust/src/websocket/parser.rs @@ -0,0 +1,76 @@ +/* Copyright (C) 2023 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 nom7::bytes::streaming::take; +use nom7::combinator::cond; +use nom7::number::streaming::{be_u16, be_u32, be_u64, be_u8}; +use nom7::IResult; + +#[derive(Clone, Debug, Default)] +pub struct WebSocketPdu { + pub fin: bool, + pub opcode: u8, + pub mask: Option, + pub payload: Vec, + pub to_skip: u64, +} + +// cf rfc6455#section-5.2 +pub fn parse_message(i: &[u8], max_pl_size: u64) -> IResult<&[u8], WebSocketPdu> { + let (i, fin_op) = be_u8(i)?; + let fin = (fin_op & 0x80) != 0; + let opcode = fin_op & 0xF; + let (i, mask_plen) = be_u8(i)?; + let mask_flag = (mask_plen & 0x80) != 0; + let (i, payload_len) = match mask_plen & 0x7F { + 126 => { + let (i, val) = be_u16(i)?; + Ok((i, val.into())) + } + 127 => be_u64(i), + _ => Ok((i, (mask_plen & 0x7F).into())), + }?; + let (i, xormask) = cond(mask_flag, take(4usize))(i)?; + let mask = if mask_flag { + let (_, m) = be_u32(xormask.unwrap())?; + Some(m) + } else { + None + }; + let (to_skip, payload_len) = if payload_len < max_pl_size { + (0, payload_len) + } else { + (payload_len - max_pl_size, max_pl_size) + }; + let (i, payload_raw) = take(payload_len)(i)?; + let mut payload = payload_raw.to_vec(); + if let Some(xorkey) = xormask { + for i in 0..payload.len() { + payload[i] ^= xorkey[i % 4]; + } + } + Ok(( + i, + WebSocketPdu { + fin, + opcode, + mask, + payload, + to_skip, + }, + )) +} diff --git a/rust/src/websocket/websocket.rs b/rust/src/websocket/websocket.rs new file mode 100644 index 000000000000..bb4459da2435 --- /dev/null +++ b/rust/src/websocket/websocket.rs @@ -0,0 +1,324 @@ +/* Copyright (C) 2023 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 super::parser; +use crate::applayer::{self, *}; +use crate::conf::conf_get; +use crate::core::{AppProto, Direction, Flow, ALPROTO_FAILED, ALPROTO_UNKNOWN, IPPROTO_TCP}; +use crate::frames::Frame; +use nom7 as nom; +use nom7::Needed; +use std; +use std::collections::VecDeque; +use std::ffi::CString; +use std::os::raw::{c_char, c_int, c_void}; + +static mut ALPROTO_WEBSOCKET: AppProto = ALPROTO_UNKNOWN; + +static mut WEBSOCKET_MAX_PAYLOAD_SIZE: u64 = 0xFFFF; + +// app-layer-frame-documentation tag start: FrameType enum +#[derive(AppLayerFrameType)] +pub enum WebSocketFrameType { + Header, + Pdu, +} + +#[derive(Default)] +pub struct WebSocketTransaction { + tx_id: u64, + pub pdu: parser::WebSocketPdu, + tx_data: AppLayerTxData, +} + +impl WebSocketTransaction { + pub fn new(direction: Direction) -> WebSocketTransaction { + Self { + tx_data: AppLayerTxData::for_direction(direction), + ..Default::default() + } + } +} + +impl Transaction for WebSocketTransaction { + fn id(&self) -> u64 { + self.tx_id + } +} + +#[derive(Default)] +pub struct WebSocketState { + state_data: AppLayerStateData, + tx_id: u64, + transactions: VecDeque, + + to_skip_tc: u64, + to_skip_ts: u64, +} + +impl State for WebSocketState { + fn get_transaction_count(&self) -> usize { + self.transactions.len() + } + + fn get_transaction_by_index(&self, index: usize) -> Option<&WebSocketTransaction> { + self.transactions.get(index) + } +} + +impl WebSocketState { + pub fn new() -> Self { + Default::default() + } + + // 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<&WebSocketTransaction> { + self.transactions.iter().find(|tx| tx.tx_id == tx_id + 1) + } + + fn new_tx(&mut self, direction: Direction) -> WebSocketTransaction { + let mut tx = WebSocketTransaction::new(direction); + self.tx_id += 1; + tx.tx_id = self.tx_id; + return tx; + } + + fn parse( + &mut self, stream_slice: StreamSlice, direction: Direction, flow: *const Flow, + ) -> AppLayerResult { + let to_skip = if direction == Direction::ToClient { + &mut self.to_skip_tc + } else { + &mut self.to_skip_ts + }; + let input = stream_slice.as_slice(); + let mut start = input; + if *to_skip > 0 { + if *to_skip >= input.len() as u64 { + *to_skip -= input.len() as u64; + return AppLayerResult::ok(); + } else { + start = &input[*to_skip as usize..]; + *to_skip = 0; + } + } + + let max_pl_size = unsafe { WEBSOCKET_MAX_PAYLOAD_SIZE }; + while !start.is_empty() { + match parser::parse_message(start, max_pl_size) { + Ok((rem, pdu)) => { + let _pdu = Frame::new( + flow, + &stream_slice, + start, + (start.len() - rem.len() - pdu.payload.len()) as i64, + WebSocketFrameType::Header as u8, + ); + let _pdu = Frame::new( + flow, + &stream_slice, + start, + (start.len() - rem.len()) as i64, + WebSocketFrameType::Pdu as u8, + ); + start = rem; + let mut tx = self.new_tx(direction); + if pdu.to_skip > 0 { + if direction == Direction::ToClient { + self.to_skip_tc = pdu.to_skip; + } else { + self.to_skip_ts = pdu.to_skip; + } + } + tx.pdu = pdu; + self.transactions.push_back(tx); + } + Err(nom::Err::Incomplete(needed)) => { + if let Needed::Size(n) = needed { + let n = usize::from(n); + // Not enough data. just ask for one more byte. + let consumed = input.len() - start.len(); + let needed = start.len() + n; + return AppLayerResult::incomplete(consumed as u32, needed as u32); + } + return AppLayerResult::err(); + } + Err(_) => { + return AppLayerResult::err(); + } + } + } + // Input was fully consumed. + return AppLayerResult::ok(); + } +} + +// C exports. + +#[no_mangle] +pub unsafe extern "C" fn rs_websocket_probing_parser( + _flow: *const Flow, _direction: u8, input: *const u8, input_len: u32, _rdir: *mut u8, +) -> AppProto { + if !input.is_null() { + let slice = build_slice!(input, input_len as usize); + if !slice.is_empty() { + // just check reserved bits are zeroed + if slice[0] & 0x70 == 0 { + return ALPROTO_WEBSOCKET; + } + return ALPROTO_FAILED; + } + } + return ALPROTO_UNKNOWN; +} + +extern "C" fn rs_websocket_state_new( + _orig_state: *mut c_void, _orig_proto: AppProto, +) -> *mut c_void { + let state = WebSocketState::new(); + let boxed = Box::new(state); + return Box::into_raw(boxed) as *mut c_void; +} + +unsafe extern "C" fn rs_websocket_state_free(state: *mut c_void) { + std::mem::drop(Box::from_raw(state as *mut WebSocketState)); +} + +unsafe extern "C" fn rs_websocket_state_tx_free(state: *mut c_void, tx_id: u64) { + let state = cast_pointer!(state, WebSocketState); + state.free_tx(tx_id); +} + +unsafe extern "C" fn rs_websocket_parse_request( + flow: *const Flow, state: *mut c_void, _pstate: *mut c_void, stream_slice: StreamSlice, + _data: *const c_void, +) -> AppLayerResult { + let state = cast_pointer!(state, WebSocketState); + state.parse(stream_slice, Direction::ToServer, flow) +} + +unsafe extern "C" fn rs_websocket_parse_response( + flow: *const Flow, state: *mut c_void, _pstate: *mut c_void, stream_slice: StreamSlice, + _data: *const c_void, +) -> AppLayerResult { + let state = cast_pointer!(state, WebSocketState); + state.parse(stream_slice, Direction::ToClient, flow) +} + +unsafe extern "C" fn rs_websocket_state_get_tx(state: *mut c_void, tx_id: u64) -> *mut c_void { + let state = cast_pointer!(state, WebSocketState); + match state.get_tx(tx_id) { + Some(tx) => { + return tx as *const _ as *mut _; + } + None => { + return std::ptr::null_mut(); + } + } +} + +unsafe extern "C" fn rs_websocket_state_get_tx_count(state: *mut c_void) -> u64 { + let state = cast_pointer!(state, WebSocketState); + return state.tx_id; +} + +unsafe extern "C" fn rs_websocket_tx_get_alstate_progress( + _tx: *mut c_void, _direction: u8, +) -> c_int { + return 1; +} + +export_tx_data_get!(rs_websocket_get_tx_data, WebSocketTransaction); +export_state_data_get!(rs_websocket_get_state_data, WebSocketState); + +// Parser name as a C style string. +const PARSER_NAME: &[u8] = b"websocket\0"; + +#[no_mangle] +pub unsafe extern "C" fn rs_websocket_register_parser() { + let parser = RustParser { + name: PARSER_NAME.as_ptr() as *const c_char, + default_port: std::ptr::null(), + ipproto: IPPROTO_TCP, + probe_ts: Some(rs_websocket_probing_parser), + probe_tc: Some(rs_websocket_probing_parser), + min_depth: 0, + max_depth: 16, + state_new: rs_websocket_state_new, + state_free: rs_websocket_state_free, + tx_free: rs_websocket_state_tx_free, + parse_ts: rs_websocket_parse_request, + parse_tc: rs_websocket_parse_response, + get_tx_count: rs_websocket_state_get_tx_count, + get_tx: rs_websocket_state_get_tx, + tx_comp_st_ts: 1, + tx_comp_st_tc: 1, + tx_get_progress: rs_websocket_tx_get_alstate_progress, + get_eventinfo: None, + get_eventinfo_byid: None, + localstorage_new: None, + localstorage_free: None, + get_tx_files: None, + get_tx_iterator: Some( + applayer::state_get_tx_iterator::, + ), + get_tx_data: rs_websocket_get_tx_data, + get_state_data: rs_websocket_get_state_data, + apply_tx_config: None, + flags: 0, // do not accept gaps as there is no good way to resync + truncate: None, + get_frame_id_by_name: Some(WebSocketFrameType::ffi_id_from_name), + get_frame_name_by_id: Some(WebSocketFrameType::ffi_name_from_id), + }; + + let ip_proto_str = CString::new("tcp").unwrap(); + + if AppLayerProtoDetectConfProtoDetectionEnabled(ip_proto_str.as_ptr(), parser.name) != 0 { + let alproto = AppLayerRegisterProtocolDetection(&parser, 1); + ALPROTO_WEBSOCKET = alproto; + if AppLayerParserConfParserEnabled(ip_proto_str.as_ptr(), parser.name) != 0 { + let _ = AppLayerRegisterParser(&parser, alproto); + } + SCLogDebug!("Rust websocket parser registered."); + if let Some(val) = conf_get("app-layer.protocols.websocket.max-payload-size") { + if let Ok(v) = val.parse::() { + WEBSOCKET_MAX_PAYLOAD_SIZE = v; + } else { + SCLogError!("Invalid value for websocket.max-payload-size"); + } + } + } else { + SCLogDebug!("Protocol detector and parser disabled for WEBSOCKET."); + } +} diff --git a/src/Makefile.am b/src/Makefile.am index 6d115ac48ae5..e20aead2c59d 100755 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -358,6 +358,7 @@ noinst_HEADERS = \ detect-urilen.h \ detect-within.h \ detect-xbits.h \ + detect-websocket.h \ device-storage.h \ feature.h \ flow-bit.h \ @@ -431,6 +432,7 @@ noinst_HEADERS = \ output-json-snmp.h \ output-json-ssh.h \ output-json-stats.h \ + output-json-websocket.h \ output-json-template.h \ output-json-tftp.h \ output-json-tls.h \ @@ -973,6 +975,7 @@ libsuricata_c_a_SOURCES = \ detect-urilen.c \ detect-within.c \ detect-xbits.c \ + detect-websocket.c \ device-storage.c \ feature.c \ flow-bit.c \ @@ -1045,6 +1048,7 @@ libsuricata_c_a_SOURCES = \ output-json-snmp.c \ output-json-ssh.c \ output-json-stats.c \ + output-json-websocket.c \ output-json-template.c \ output-json-tftp.c \ output-json-tls.c \ diff --git a/src/app-layer-detect-proto.c b/src/app-layer-detect-proto.c index 690950d34e72..31bee23f9a92 100644 --- a/src/app-layer-detect-proto.c +++ b/src/app-layer-detect-proto.c @@ -579,7 +579,10 @@ static AppProto AppLayerProtoDetectPPGetProto(Flow *f, const uint8_t *buf, uint3 } } - if (dir == STREAM_TOSERVER && f->alproto_tc != ALPROTO_UNKNOWN) { + if (f->alproto_expect != ALPROTO_UNKNOWN) { + // used for websocket which does not use ports + pe0 = AppLayerProtoDetectGetProbingParser(alpd_ctx.ctx_pp, ipproto, f->alproto_expect); + } else if (dir == STREAM_TOSERVER && f->alproto_tc != ALPROTO_UNKNOWN) { pe0 = AppLayerProtoDetectGetProbingParser(alpd_ctx.ctx_pp, ipproto, f->alproto_tc); } else if (dir == STREAM_TOCLIENT && f->alproto_ts != ALPROTO_UNKNOWN) { pe0 = AppLayerProtoDetectGetProbingParser(alpd_ctx.ctx_pp, ipproto, f->alproto_ts); @@ -1574,6 +1577,13 @@ void AppLayerProtoDetectPPRegister(uint8_t ipproto, SCEnter(); DetectPort *head = NULL; + if (portstr == NULL) { + // WebSocket has a probing parser, but no port + // as it works only on HTTP1 protocol upgrade + AppLayerProtoDetectInsertNewProbingParser(&alpd_ctx.ctx_pp, ipproto, 0, alproto, min_depth, + max_depth, direction, ProbingParser1, ProbingParser2); + return; + } DetectPortParse(NULL,&head, portstr); DetectPort *temp_dp = head; while (temp_dp != NULL) { diff --git a/src/app-layer-htp.c b/src/app-layer-htp.c index 5d48611812c1..5ccbac83cf1c 100644 --- a/src/app-layer-htp.c +++ b/src/app-layer-htp.c @@ -53,6 +53,7 @@ #include "app-layer-protos.h" #include "app-layer-parser.h" +#include "app-layer-expectation.h" #include "app-layer.h" #include "app-layer-detect-proto.h" @@ -975,11 +976,7 @@ static AppLayerResult HTPHandleResponseData(Flow *f, void *htp_state, AppLayerPa if (tx != NULL && tx->response_status_number == 101) { htp_header_t *h = (htp_header_t *)htp_table_get_c(tx->response_headers, "Upgrade"); - if (h == NULL || bstr_cmp_c(h->value, "h2c") != 0) { - break; - } - if (AppLayerProtoDetectGetProtoName(ALPROTO_HTTP2) == NULL) { - // if HTTP2 is disabled, keep the HTP_STREAM_TUNNEL mode + if (h == NULL) { break; } uint16_t dp = 0; @@ -987,17 +984,39 @@ static AppLayerResult HTPHandleResponseData(Flow *f, void *htp_state, AppLayerPa dp = (uint16_t)tx->request_port_number; } consumed = htp_connp_res_data_consumed(hstate->connp); - hstate->slice = NULL; - if (!AppLayerRequestProtocolChange(hstate->f, dp, ALPROTO_HTTP2)) { - HTPSetEvent(hstate, NULL, STREAM_TOCLIENT, - HTTP_DECODER_EVENT_FAILED_PROTOCOL_CHANGE); - } - // During HTTP2 upgrade, we may consume the HTTP1 part of the data - // and we need to parser the remaining part with HTTP2 - if (consumed > 0 && consumed < input_len) { - SCReturnStruct(APP_LAYER_INCOMPLETE(consumed, input_len - consumed)); + if (bstr_cmp_c(h->value, "h2c") == 0) { + if (AppLayerProtoDetectGetProtoName(ALPROTO_HTTP2) == NULL) { + // if HTTP2 is disabled, keep the HTP_STREAM_TUNNEL mode + break; + } + hstate->slice = NULL; + if (!AppLayerRequestProtocolChange(hstate->f, dp, ALPROTO_HTTP2)) { + HTPSetEvent(hstate, NULL, STREAM_TOCLIENT, + HTTP_DECODER_EVENT_FAILED_PROTOCOL_CHANGE); + } + // During HTTP2 upgrade, we may consume the HTTP1 part of the data + // and we need to parser the remaining part with HTTP2 + if (consumed > 0 && consumed < input_len) { + SCReturnStruct(APP_LAYER_INCOMPLETE(consumed, input_len - consumed)); + } + SCReturnStruct(APP_LAYER_OK); + } else if (bstr_cmp_c_nocase(h->value, "WebSocket") == 0) { + if (AppLayerProtoDetectGetProtoName(ALPROTO_WEBSOCKET) == NULL) { + // if WS is disabled, keep the HTP_STREAM_TUNNEL mode + break; + } + hstate->slice = NULL; + if (!AppLayerRequestProtocolChange(hstate->f, dp, ALPROTO_WEBSOCKET)) { + HTPSetEvent(hstate, NULL, STREAM_TOCLIENT, + HTTP_DECODER_EVENT_FAILED_PROTOCOL_CHANGE); + } + // During WS upgrade, we may consume the HTTP1 part of the data + // and we need to parser the remaining part with WS + if (consumed > 0 && consumed < input_len) { + SCReturnStruct(APP_LAYER_INCOMPLETE(consumed, input_len - consumed)); + } + SCReturnStruct(APP_LAYER_OK); } - SCReturnStruct(APP_LAYER_OK); } break; default: diff --git a/src/app-layer-parser.c b/src/app-layer-parser.c index 1f6066471757..109e688baeee 100644 --- a/src/app-layer-parser.c +++ b/src/app-layer-parser.c @@ -1764,6 +1764,7 @@ void AppLayerParserRegisterProtocolParsers(void) RegisterSNMPParsers(); RegisterSIPParsers(); RegisterQuicParsers(); + rs_websocket_register_parser(); rs_template_register_parser(); RegisterRFBParsers(); RegisterMQTTParsers(); diff --git a/src/app-layer-protos.c b/src/app-layer-protos.c index 368efacd88d7..b6e1b73d08d4 100644 --- a/src/app-layer-protos.c +++ b/src/app-layer-protos.c @@ -60,6 +60,7 @@ const AppProtoStringTuple AppProtoStrings[ALPROTO_MAX] = { { ALPROTO_MQTT, "mqtt" }, { ALPROTO_PGSQL, "pgsql" }, { ALPROTO_TELNET, "telnet" }, + { ALPROTO_WEBSOCKET, "websocket" }, { ALPROTO_TEMPLATE, "template" }, { ALPROTO_RDP, "rdp" }, { ALPROTO_HTTP2, "http2" }, diff --git a/src/app-layer-protos.h b/src/app-layer-protos.h index dd372550cbf5..5ecc5d88d31a 100644 --- a/src/app-layer-protos.h +++ b/src/app-layer-protos.h @@ -56,6 +56,7 @@ enum AppProtoEnum { ALPROTO_MQTT, ALPROTO_PGSQL, ALPROTO_TELNET, + ALPROTO_WEBSOCKET, ALPROTO_TEMPLATE, ALPROTO_RDP, ALPROTO_HTTP2, diff --git a/src/detect-engine-register.c b/src/detect-engine-register.c index a97da4617197..606a04ce8a6d 100644 --- a/src/detect-engine-register.c +++ b/src/detect-engine-register.c @@ -236,6 +236,7 @@ #include "detect-quic-version.h" #include "detect-quic-cyu-hash.h" #include "detect-quic-cyu-string.h" +#include "detect-websocket.h" #include "detect-bypass.h" #include "detect-ftpdata.h" @@ -698,6 +699,7 @@ void SigTableSetup(void) DetectQuicVersionRegister(); DetectQuicCyuHashRegister(); DetectQuicCyuStringRegister(); + DetectWebsocketRegister(); DetectBypassRegister(); DetectConfigRegister(); diff --git a/src/detect-engine-register.h b/src/detect-engine-register.h index 2e4a330788ed..2ea81b7aa529 100644 --- a/src/detect-engine-register.h +++ b/src/detect-engine-register.h @@ -314,6 +314,10 @@ enum DetectKeywordId { DETECT_AL_QUIC_UA, DETECT_AL_QUIC_CYU_HASH, DETECT_AL_QUIC_CYU_STRING, + DETECT_WEBSOCKET_MASK, + DETECT_WEBSOCKET_OPCODE, + DETECT_WEBSOCKET_FIN, + DETECT_WEBSOCKET_PAYLOAD, DETECT_BYPASS, diff --git a/src/detect-websocket.c b/src/detect-websocket.c new file mode 100644 index 000000000000..f48f1eaedd85 --- /dev/null +++ b/src/detect-websocket.c @@ -0,0 +1,249 @@ +/* Copyright (C) 2023 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 Philippe Antoine + */ + +#include "suricata-common.h" +#include "detect.h" +#include "detect-parse.h" +#include "detect-engine.h" +#include "detect-engine-content-inspection.h" +#include "detect-engine-uint.h" +#include "detect-engine-prefilter.h" +#include "detect-websocket.h" + +#include "rust.h" + +static int websocket_tx_id = 0; +static int websocket_payload_id = 0; + +/** + * \internal + * \brief this function will free memory associated with DetectWebSocketOpcodeData + * + * \param de pointer to DetectWebSocketOpcodeData + */ +static void DetectWebSocketOpcodeFree(DetectEngineCtx *de_ctx, void *de_ptr) +{ + rs_detect_u8_free(de_ptr); +} + +/** + * \internal + * \brief Function to match opcode of a websocket tx + * + * \param det_ctx Pointer to the pattern matcher thread. + * \param f Pointer to the current flow. + * \param flags Flags. + * \param state App layer state. + * \param txv Pointer to the transaction. + * \param s Pointer to the Signature. + * \param ctx Pointer to the sigmatch that we will cast into DetectWebSocketOpcodeData. + * + * \retval 0 no match. + * \retval 1 match. + */ +static int DetectWebSocketOpcodeMatch(DetectEngineThreadCtx *det_ctx, Flow *f, uint8_t flags, + void *state, void *txv, const Signature *s, const SigMatchCtx *ctx) +{ + const DetectU8Data *de = (const DetectU8Data *)ctx; + uint8_t opc = SCWebSocketGetOpcode(txv); + return DetectU8Match(opc, de); +} + +/** + * \internal + * \brief this function is used to add the parsed sigmatch into the current signature + * + * \param de_ctx pointer to the Detection Engine Context + * \param s pointer to the Current Signature + * \param rawstr pointer to the user provided options + * + * \retval 0 on Success + * \retval -1 on Failure + */ +static int DetectWebSocketOpcodeSetup(DetectEngineCtx *de_ctx, Signature *s, const char *rawstr) +{ + if (DetectSignatureSetAppProto(s, ALPROTO_WEBSOCKET) < 0) + return -1; + + DetectU8Data *de = SCWebSocketParseOpcode(rawstr); + if (de == NULL) + return -1; + + if (SigMatchAppendSMToList( + de_ctx, s, DETECT_WEBSOCKET_OPCODE, (SigMatchCtx *)de, websocket_tx_id) == NULL) { + DetectWebSocketOpcodeFree(de_ctx, de); + return -1; + } + + return 0; +} + +/** + * \internal + * \brief this function will free memory associated with DetectWebSocketMaskData + * + * \param de pointer to DetectWebSocketMaskData + */ +static void DetectWebSocketMaskFree(DetectEngineCtx *de_ctx, void *de_ptr) +{ + rs_detect_u32_free(de_ptr); +} + +static int DetectWebSocketMaskMatch(DetectEngineThreadCtx *det_ctx, Flow *f, uint8_t flags, + void *state, void *txv, const Signature *s, const SigMatchCtx *ctx) +{ + uint32_t val; + const DetectU32Data *du32 = (const DetectU32Data *)ctx; + if (SCWebSocketGetMask(txv, &val)) { + return DetectU32Match(val, du32); + } + return 0; +} + +static int DetectWebSocketMaskSetup(DetectEngineCtx *de_ctx, Signature *s, const char *rawstr) +{ + if (DetectSignatureSetAppProto(s, ALPROTO_WEBSOCKET) < 0) + return -1; + + DetectU32Data *du32 = DetectU32Parse(rawstr); + if (du32 == NULL) + return -1; + + if (SigMatchAppendSMToList( + de_ctx, s, DETECT_WEBSOCKET_MASK, (SigMatchCtx *)du32, websocket_tx_id) == NULL) { + DetectWebSocketMaskFree(de_ctx, du32); + return -1; + } + + return 0; +} + +static int DetectWebSocketFinMatch(DetectEngineThreadCtx *det_ctx, Flow *f, uint8_t flags, + void *state, void *txv, const Signature *s, const SigMatchCtx *ctx) +{ + bool mask = SCWebSocketGetFin(txv); + if ((mask && ctx) || (!mask && ctx == NULL)) { + return 1; + } + return 0; +} + +static int DetectWebSocketFinSetup(DetectEngineCtx *de_ctx, Signature *s, const char *rawstr) +{ + if (DetectSignatureSetAppProto(s, ALPROTO_WEBSOCKET) < 0) + return -1; + + void *dummyptr = NULL; + if (strcmp(rawstr, "true") == 0) { + dummyptr = de_ctx; + } else if (strcmp(rawstr, "false") != 0) { + SCLogError("invalid websocket.mask boolean value: %s", rawstr); + } + + if (SigMatchAppendSMToList(de_ctx, s, DETECT_WEBSOCKET_FIN, (SigMatchCtx *)dummyptr, + websocket_tx_id) == NULL) { + return -1; + } + + return 0; +} + +static int DetectWebSocketPayloadSetup(DetectEngineCtx *de_ctx, Signature *s, const char *rulestr) +{ + if (DetectBufferSetActiveList(de_ctx, s, websocket_payload_id) < 0) + return -1; + + if (DetectSignatureSetAppProto(s, ALPROTO_WEBSOCKET) != 0) + return -1; + + return 0; +} + +static InspectionBuffer *GetData(DetectEngineThreadCtx *det_ctx, + const DetectEngineTransforms *transforms, Flow *_f, const uint8_t _flow_flags, void *txv, + const int list_id) +{ + InspectionBuffer *buffer = InspectionBufferGet(det_ctx, list_id); + if (buffer->inspect == NULL) { + const uint8_t *b = NULL; + uint32_t b_len = 0; + + if (!SCWebSocketGetPayload(txv, &b, &b_len)) + return NULL; + if (b == NULL || b_len == 0) + return NULL; + + InspectionBufferSetup(det_ctx, list_id, buffer, b, b_len); + InspectionBufferApplyTransforms(buffer, transforms); + } + return buffer; +} + +/** + * \brief Registration function for websocket.opcode: keyword + */ +void DetectWebsocketRegister(void) +{ + sigmatch_table[DETECT_WEBSOCKET_OPCODE].name = "websocket.opcode"; + sigmatch_table[DETECT_WEBSOCKET_OPCODE].desc = "match WebSocket opcode"; + sigmatch_table[DETECT_WEBSOCKET_OPCODE].url = "/rules/websocket-keywords.html#websocket-opcode"; + sigmatch_table[DETECT_WEBSOCKET_OPCODE].AppLayerTxMatch = DetectWebSocketOpcodeMatch; + sigmatch_table[DETECT_WEBSOCKET_OPCODE].Setup = DetectWebSocketOpcodeSetup; + sigmatch_table[DETECT_WEBSOCKET_OPCODE].Free = DetectWebSocketOpcodeFree; + + DetectAppLayerInspectEngineRegister("websocket.tx", ALPROTO_WEBSOCKET, SIG_FLAG_TOSERVER, 1, + DetectEngineInspectGenericList, NULL); + DetectAppLayerInspectEngineRegister("websocket.tx", ALPROTO_WEBSOCKET, SIG_FLAG_TOCLIENT, 1, + DetectEngineInspectGenericList, NULL); + + websocket_tx_id = DetectBufferTypeGetByName("websocket.tx"); + + sigmatch_table[DETECT_WEBSOCKET_MASK].name = "websocket.mask"; + sigmatch_table[DETECT_WEBSOCKET_MASK].desc = "match WebSocket mask"; + sigmatch_table[DETECT_WEBSOCKET_MASK].url = "/rules/websocket-keywords.html#websocket-mask"; + sigmatch_table[DETECT_WEBSOCKET_MASK].AppLayerTxMatch = DetectWebSocketMaskMatch; + sigmatch_table[DETECT_WEBSOCKET_MASK].Setup = DetectWebSocketMaskSetup; + sigmatch_table[DETECT_WEBSOCKET_MASK].Free = DetectWebSocketMaskFree; + + sigmatch_table[DETECT_WEBSOCKET_FIN].name = "websocket.fin"; + sigmatch_table[DETECT_WEBSOCKET_FIN].desc = "match WebSocket fin flag"; + sigmatch_table[DETECT_WEBSOCKET_FIN].url = "/rules/websocket-keywords.html#websocket-fin"; + sigmatch_table[DETECT_WEBSOCKET_FIN].AppLayerTxMatch = DetectWebSocketFinMatch; + sigmatch_table[DETECT_WEBSOCKET_FIN].Setup = DetectWebSocketFinSetup; + + sigmatch_table[DETECT_WEBSOCKET_PAYLOAD].name = "websocket.payload"; + sigmatch_table[DETECT_WEBSOCKET_PAYLOAD].desc = "match WebSocket payload"; + sigmatch_table[DETECT_WEBSOCKET_PAYLOAD].url = + "/rules/websocket-keywords.html#websocket-payload"; + sigmatch_table[DETECT_WEBSOCKET_PAYLOAD].Setup = DetectWebSocketPayloadSetup; + sigmatch_table[DETECT_WEBSOCKET_PAYLOAD].flags |= SIGMATCH_NOOPT; + DetectAppLayerInspectEngineRegister("websocket.payload", ALPROTO_WEBSOCKET, SIG_FLAG_TOSERVER, + 0, DetectEngineInspectBufferGeneric, GetData); + DetectAppLayerInspectEngineRegister("websocket.payload", ALPROTO_WEBSOCKET, SIG_FLAG_TOCLIENT, + 0, DetectEngineInspectBufferGeneric, GetData); + DetectAppLayerMpmRegister("websocket.payload", SIG_FLAG_TOSERVER, 2, + PrefilterGenericMpmRegister, GetData, ALPROTO_WEBSOCKET, 1); + DetectAppLayerMpmRegister("websocket.payload", SIG_FLAG_TOCLIENT, 2, + PrefilterGenericMpmRegister, GetData, ALPROTO_WEBSOCKET, 1); + websocket_payload_id = DetectBufferTypeGetByName("websocket.payload"); +} diff --git a/src/detect-websocket.h b/src/detect-websocket.h new file mode 100644 index 000000000000..54e8a22ae4a8 --- /dev/null +++ b/src/detect-websocket.h @@ -0,0 +1,29 @@ +/* Copyright (C) 2023 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 Philippe Antoine + */ + +#ifndef __DETECT_WEBSOCKET_H__ +#define __DETECT_WEBSOCKET_H__ + +void DetectWebsocketRegister(void); + +#endif /* __DETECT_WEBSOCKET_H__ */ diff --git a/src/output-json-websocket.c b/src/output-json-websocket.c new file mode 100644 index 000000000000..9878bcc74ba6 --- /dev/null +++ b/src/output-json-websocket.c @@ -0,0 +1,160 @@ +/* Copyright (C) 2023 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 Philippe Antoine + * + * Implement JSON/eve logging app-layer WebSocket. + */ + +#include "suricata-common.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 "output-json-websocket.h" +#include "rust.h" + +typedef struct LogWebSocketFileCtx_ { + uint32_t flags; + OutputJsonCtx *eve_ctx; +} LogWebSocketFileCtx; + +typedef struct LogWebSocketLogThread_ { + LogWebSocketFileCtx *websocketlog_ctx; + OutputJsonThreadCtx *ctx; +} LogWebSocketLogThread; + +static int JsonWebSocketLogger(ThreadVars *tv, void *thread_data, const Packet *p, Flow *f, + void *state, void *tx, uint64_t tx_id) +{ + LogWebSocketLogThread *thread = thread_data; + + JsonBuilder *js = CreateEveHeader( + p, LOG_DIR_PACKET, "websocket", NULL, thread->websocketlog_ctx->eve_ctx); + if (unlikely(js == NULL)) { + return TM_ECODE_FAILED; + } + + if (!rs_websocket_logger_log(tx, js)) { + goto error; + } + + OutputJsonBuilderBuffer(js, thread->ctx); + jb_free(js); + + return TM_ECODE_OK; + +error: + jb_free(js); + return TM_ECODE_FAILED; +} + +static void OutputWebSocketLogDeInitCtxSub(OutputCtx *output_ctx) +{ + LogWebSocketFileCtx *websocketlog_ctx = (LogWebSocketFileCtx *)output_ctx->data; + SCFree(websocketlog_ctx); + SCFree(output_ctx); +} + +static OutputInitResult OutputWebSocketLogInitSub(ConfNode *conf, OutputCtx *parent_ctx) +{ + OutputInitResult result = { NULL, false }; + OutputJsonCtx *ajt = parent_ctx->data; + + LogWebSocketFileCtx *websocketlog_ctx = SCCalloc(1, sizeof(*websocketlog_ctx)); + if (unlikely(websocketlog_ctx == NULL)) { + return result; + } + websocketlog_ctx->eve_ctx = ajt; + + OutputCtx *output_ctx = SCCalloc(1, sizeof(*output_ctx)); + if (unlikely(output_ctx == NULL)) { + SCFree(websocketlog_ctx); + return result; + } + output_ctx->data = websocketlog_ctx; + output_ctx->DeInit = OutputWebSocketLogDeInitCtxSub; + + AppLayerParserRegisterLogger(IPPROTO_TCP, ALPROTO_WEBSOCKET); + + result.ctx = output_ctx; + result.ok = true; + return result; +} + +static TmEcode JsonWebSocketLogThreadInit(ThreadVars *t, const void *initdata, void **data) +{ + LogWebSocketLogThread *thread = SCCalloc(1, sizeof(*thread)); + if (unlikely(thread == NULL)) { + return TM_ECODE_FAILED; + } + + if (initdata == NULL) { + SCLogDebug("Error getting context for EveLogWebSocket. \"initdata\" is NULL."); + goto error_exit; + } + + thread->websocketlog_ctx = ((OutputCtx *)initdata)->data; + thread->ctx = CreateEveThreadCtx(t, thread->websocketlog_ctx->eve_ctx); + if (!thread->ctx) { + goto error_exit; + } + *data = (void *)thread; + + return TM_ECODE_OK; + +error_exit: + SCFree(thread); + return TM_ECODE_FAILED; +} + +static TmEcode JsonWebSocketLogThreadDeinit(ThreadVars *t, void *data) +{ + LogWebSocketLogThread *thread = (LogWebSocketLogThread *)data; + if (thread == NULL) { + return TM_ECODE_OK; + } + FreeEveThreadCtx(thread->ctx); + SCFree(thread); + return TM_ECODE_OK; +} + +void JsonWebSocketLogRegister(void) +{ + /* Register as an eve sub-module. */ + OutputRegisterTxSubModule(LOGGER_JSON_TX, "eve-log", "JsonWebSocketLog", "eve-log.websocket", + OutputWebSocketLogInitSub, ALPROTO_WEBSOCKET, JsonWebSocketLogger, + JsonWebSocketLogThreadInit, JsonWebSocketLogThreadDeinit, NULL); +} diff --git a/src/output-json-websocket.h b/src/output-json-websocket.h new file mode 100644 index 000000000000..481df78c0aee --- /dev/null +++ b/src/output-json-websocket.h @@ -0,0 +1,29 @@ +/* Copyright (C) 2023 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_WEBSOCKET_H__ +#define __OUTPUT_JSON_WEBSOCKET_H__ + +void JsonWebSocketLogRegister(void); + +#endif /* __OUTPUT_JSON_WEBSOCKET_H__ */ diff --git a/src/output.c b/src/output.c index 149dda58c284..df8a18f91ffd 100644 --- a/src/output.c +++ b/src/output.c @@ -80,6 +80,7 @@ #include "output-json-rfb.h" #include "output-json-mqtt.h" #include "output-json-pgsql.h" +#include "output-json-websocket.h" #include "output-json-template.h" #include "output-json-rdp.h" #include "output-json-http2.h" @@ -1117,6 +1118,8 @@ void OutputRegisterLoggers(void) JsonMQTTLogRegister(); /* Pgsql JSON logger. */ JsonPgsqlLogRegister(); + /* WebSocket JSON logger. */ + JsonWebSocketLogRegister(); /* Template JSON logger. */ JsonTemplateLogRegister(); /* RDP JSON logger. */ @@ -1159,6 +1162,7 @@ static EveJsonSimpleAppLayerLogger simple_json_applayer_loggers[ALPROTO_MAX] = { { ALPROTO_MQTT, JsonMQTTAddMetadata }, { ALPROTO_PGSQL, NULL }, // TODO missing { ALPROTO_TELNET, NULL }, // no logging + { ALPROTO_WEBSOCKET, rs_websocket_logger_log }, { ALPROTO_TEMPLATE, rs_template_logger_log }, { ALPROTO_RDP, (EveJsonSimpleTxLogFunc)rs_rdp_to_json }, { ALPROTO_HTTP2, rs_http2_log_json }, diff --git a/suricata.yaml.in b/suricata.yaml.in index 630399126dbe..181008b7086c 100644 --- a/suricata.yaml.in +++ b/suricata.yaml.in @@ -279,6 +279,7 @@ outputs: #md5: [body, subject] #- dnp3 + - websocket - ftp - rdp - nfs @@ -923,6 +924,10 @@ app-layer: ftp: enabled: yes # memcap: 64mb + websocket: + #enabled: yes + # Maximum used payload size, the rest is skipped + # max-payload-size: 65535 rdp: #enabled: yes ssh: