From: Philippe Antoine Date: Fri, 22 Dec 2023 11:01:33 +0000 (+0100) Subject: app-layer: websockets protocol support X-Git-Tag: suricata-8.0.0-beta1~1462 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=44b6aa5e4b3399e0fba13fefbbd16ebd025e8cd2;p=thirdparty%2Fsuricata.git app-layer: websockets protocol support Ticket: 2695 --- diff --git a/doc/userguide/rules/index.rst b/doc/userguide/rules/index.rst index cf01e14d1f..582520ebf9 100644 --- a/doc/userguide/rules/index.rst +++ b/doc/userguide/rules/index.rst @@ -36,6 +36,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 9f87796253..41f7fe0b83 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 0000000000..598bfe99fa --- /dev/null +++ b/doc/userguide/rules/websocket-keywords.rst @@ -0,0 +1,63 @@ +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.flags +--------------- + +Matches on the websocket flags. +It uses a 8-bit unsigned integer as value. +Only the four upper bits are used. + +The value can also be a list of strings (comma-separated), +where each string is the name of a specific bit like `fin` and `comp`, +and can be prefixed by `!` for negation. + +websocket.flags uses an :ref:`unsigned 8-bits integer ` + +Examples:: + + websocket.flags:128; + websocket.flags:&0x40=0x40; + websocket.flags:fin,!comp; + +websocket.mask +-------------- + +Matches on the websocket mask if any. +It uses a 32-bit unsigned integer as value (big-endian). + +websocket.mask uses an :ref:`unsigned 32-bits integer ` + +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 + +websocket.opcode uses an :ref:`unsigned 8-bits integer ` + +Examples:: + + websocket.opcode:1; + websocket.opcode:>8; + websocket.opcode:ping; diff --git a/etc/schema.json b/etc/schema.json index 5fc3e8f8e6..451b1a6f63 100644 --- a/etc/schema.json +++ b/etc/schema.json @@ -3898,6 +3898,9 @@ "tls": { "description": "Errors encountered parsing TLS protocol", "$ref": "#/$defs/stats_applayer_error" + }, + "websocket": { + "$ref": "#/$defs/stats_applayer_error" } }, "additionalProperties": false @@ -4056,6 +4059,9 @@ "tls": { "description": "Number of flows for TLS protocol", "type": "integer" + }, + "websocket": { + "type": "integer" } }, "additionalProperties": false @@ -4170,6 +4176,9 @@ }, "tls": { "type": "integer" + }, + "websocket": { + "type": "integer" } }, "additionalProperties": false @@ -5653,6 +5662,21 @@ } }, "additionalProperties": false + }, + "websocket": { + "type": "object", + "properties": { + "fin": { + "type": "boolean" + }, + "mask": { + "type": "integer" + }, + "opcode": { + "type": "string" + } + }, + "additionalProperties": false } }, "$defs": { diff --git a/rules/Makefile.am b/rules/Makefile.am index d0ea6eda62..cba0aa370a 100644 --- a/rules/Makefile.am +++ b/rules/Makefile.am @@ -22,4 +22,5 @@ smb-events.rules \ smtp-events.rules \ ssh-events.rules \ stream-events.rules \ -tls-events.rules +tls-events.rules \ +websocket-events.rules diff --git a/rules/websocket-events.rules b/rules/websocket-events.rules new file mode 100644 index 0000000000..3acc21132e --- /dev/null +++ b/rules/websocket-events.rules @@ -0,0 +1,8 @@ +# WebSocket app-layer event rules. +# +# These SIDs fall in the 2235000+ range. See: +# http://doc.emergingthreats.net/bin/view/Main/SidAllocation and +# https://redmine.openinfosecfoundation.org/projects/suricata/wiki/AppLayer + +alert websocket any any -> any any (msg:"SURICATA Websocket skipped end of payload"; app-layer-event:websocket.skip_end_of_payload; classtype:protocol-command-decode; sid:2235000; rev:1;) +alert websocket any any -> any any (msg:"SURICATA Websocket reassembly limit reached"; app-layer-event:websocket.reassembly_limit_reached; classtype:protocol-command-decode; sid:2235001; rev:1;) diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 9e58d4d805..1dd16100ce 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -108,6 +108,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 0000000000..44737ae6f0 --- /dev/null +++ b/rust/src/websocket/detect.rs @@ -0,0 +1,135 @@ +/* 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::detect::uint::{ + detect_parse_uint, detect_parse_uint_enum, DetectUintData, DetectUintMode, +}; +use crate::websocket::parser::WebSocketOpcode; + +use nom7::branch::alt; +use nom7::bytes::complete::{is_a, tag}; +use nom7::combinator::{opt, value}; +use nom7::multi::many1; +use nom7::IResult; + +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 SCWebSocketGetFlags(tx: &mut WebSocketTransaction) -> u8 { + return tx.pdu.flags; +} + +#[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 Some(ctx) = detect_parse_uint_enum::(s) { + let boxed = Box::new(ctx); + return Box::into_raw(boxed) as *mut _; + } + } + return std::ptr::null_mut(); +} + +struct WebSocketFlag { + neg: bool, + value: u8, +} + +fn parse_flag_list_item(s: &str) -> IResult<&str, WebSocketFlag> { + let (s, _) = opt(is_a(" "))(s)?; + let (s, neg) = opt(tag("!"))(s)?; + let neg = neg.is_some(); + let (s, value) = alt((value(0x80, tag("fin")), value(0x40, tag("comp"))))(s)?; + let (s, _) = opt(is_a(" ,"))(s)?; + Ok((s, WebSocketFlag { neg, value })) +} + +fn parse_flag_list(s: &str) -> IResult<&str, Vec> { + return many1(parse_flag_list_item)(s); +} + +fn parse_flags(s: &str) -> Option> { + // try first numerical value + if let Ok((_, ctx)) = detect_parse_uint::(s) { + return Some(ctx); + } + // otherwise, try strings for bitmask + if let Ok((_, l)) = parse_flag_list(s) { + let mut arg1 = 0; + let mut arg2 = 0; + for elem in l.iter() { + if elem.value & arg1 != 0 { + SCLogWarning!("Repeated bitflag for websocket.flags"); + return None; + } + arg1 |= elem.value; + if !elem.neg { + arg2 |= elem.value; + } + } + let ctx = DetectUintData:: { + arg1, + arg2, + mode: DetectUintMode::DetectUintModeBitmask, + }; + return Some(ctx); + } + return None; +} + +#[no_mangle] +pub unsafe extern "C" fn SCWebSocketParseFlags( + 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 Some(ctx) = parse_flags(s) { + 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 0000000000..ab8b519626 --- /dev/null +++ b/rust/src/websocket/logger.rs @@ -0,0 +1,45 @@ +/* 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::WebSocketOpcode; +use super::websocket::WebSocketTransaction; +use crate::detect::EnumString; +use crate::jsonbuilder::{JsonBuilder, JsonError}; +use std; + +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(opcode) = WebSocketOpcode::from_u(tx.pdu.opcode) { + js.set_string("opcode", opcode.to_str())?; + } 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 0000000000..c57660f2a4 --- /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 0000000000..edf611707e --- /dev/null +++ b/rust/src/websocket/parser.rs @@ -0,0 +1,96 @@ +/* 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; +use suricata_derive::EnumStringU8; + +#[derive(Clone, Debug, Default, EnumStringU8)] +#[repr(u8)] +pub enum WebSocketOpcode { + #[default] + Continuation = 0, + Text = 1, + Binary = 2, + Ping = 8, + Pong = 9, +} + +#[derive(Clone, Debug, Default)] +pub struct WebSocketPdu { + pub flags: u8, + pub fin: bool, + pub compress: 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: u32) -> IResult<&[u8], WebSocketPdu> { + let (i, flags_op) = be_u8(i)?; + let fin = (flags_op & 0x80) != 0; + let compress = (flags_op & 0x40) != 0; + let flags = flags_op & 0xF0; + let opcode = flags_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 + }; + // we limit payload_len to u32, so as to build on 32-bit system + // where we cannot take(usize) with a u64 + let (to_skip, payload_len) = if payload_len < max_pl_size.into() { + (0, payload_len as u32) + } else { + (payload_len - (max_pl_size as u64), 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 { + flags, + fin, + compress, + opcode, + mask, + payload, + to_skip, + }, + )) +} diff --git a/rust/src/websocket/websocket.rs b/rust/src/websocket/websocket.rs new file mode 100644 index 0000000000..4e94ea79b4 --- /dev/null +++ b/rust/src/websocket/websocket.rs @@ -0,0 +1,383 @@ +/* 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 flate2::read::DeflateDecoder; + +use std; +use std::collections::VecDeque; +use std::ffi::CString; +use std::io::Read; +use std::os::raw::{c_char, c_int, c_void}; + +static mut ALPROTO_WEBSOCKET: AppProto = ALPROTO_UNKNOWN; + +static mut WEBSOCKET_MAX_PAYLOAD_SIZE: u32 = 0xFFFF; + +// app-layer-frame-documentation tag start: FrameType enum +#[derive(AppLayerFrameType)] +pub enum WebSocketFrameType { + Header, + Pdu, +} + +#[derive(AppLayerEvent)] +pub enum WebSocketEvent { + SkipEndOfPayload, + ReassemblyLimitReached, +} + +#[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)] +struct WebSocketReassemblyBuffer { + data: Vec, + compress: bool, +} + +#[derive(Default)] +pub struct WebSocketState { + state_data: AppLayerStateData, + tx_id: u64, + transactions: VecDeque, + + c2s_buf: WebSocketReassemblyBuffer, + s2c_buf: WebSocketReassemblyBuffer, + + 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.tx_data.set_event(WebSocketEvent::SkipEndOfPayload as u8); + } + let buf = if direction == Direction::ToClient { + &mut self.s2c_buf + } else { + &mut self.c2s_buf + }; + if !buf.data.is_empty() || !pdu.fin { + if buf.data.is_empty() { + buf.compress = pdu.compress; + } + if buf.data.len() + pdu.payload.len() < max_pl_size as usize { + buf.data.extend(&pdu.payload); + } else if buf.data.len() < max_pl_size as usize { + buf.data + .extend(&pdu.payload[..max_pl_size as usize - buf.data.len()]); + tx.tx_data + .set_event(WebSocketEvent::ReassemblyLimitReached as u8); + } + } + tx.pdu = pdu; + if tx.pdu.fin && !buf.data.is_empty() { + // the final PDU gets the full reassembled payload + std::mem::swap(&mut tx.pdu.payload, &mut buf.data); + buf.data.clear(); + } + if buf.compress && tx.pdu.fin { + buf.compress = false; + // cf RFC 7692 section-7.2.2 + tx.pdu.payload.extend_from_slice(&[0, 0, 0xFF, 0xFF]); + let mut deflater = DeflateDecoder::new(&tx.pdu.payload[..]); + let mut v = Vec::new(); + // do not check result because + // deflate with rust backend fails on good input cf https://github.com/rust-lang/flate2-rs/issues/389 + let _ = deflater.read_to_end(&mut v); + if !v.is_empty() { + std::mem::swap(&mut tx.pdu.payload, &mut v); + } + } + 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, except RSV1 + // as RSV1 is used for compression cf RFC 7692 + if slice[0] & 0x30 == 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: Some(WebSocketEvent::get_event_info), + get_eventinfo_byid: Some(WebSocketEvent::get_event_info_by_id), + 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"); + } + } + AppLayerParserRegisterLogger(IPPROTO_TCP, ALPROTO_WEBSOCKET); + } else { + SCLogDebug!("Protocol detector and parser disabled for WEBSOCKET."); + } +} diff --git a/src/Makefile.am b/src/Makefile.am index d96e8a0138..7dd33a7599 100755 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -362,6 +362,7 @@ noinst_HEADERS = \ detect-urilen.h \ detect-within.h \ detect-xbits.h \ + detect-websocket.h \ device-storage.h \ feature.h \ flow-bit.h \ @@ -975,6 +976,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 \ diff --git a/src/app-layer-htp.c b/src/app-layer-htp.c index 94d4c38fbd..a325f34c22 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" @@ -979,11 +980,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; @@ -991,17 +988,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 b92562ef49..cf04151d79 100644 --- a/src/app-layer-parser.c +++ b/src/app-layer-parser.c @@ -1754,6 +1754,7 @@ void AppLayerParserRegisterProtocolParsers(void) RegisterSNMPParsers(); RegisterSIPParsers(); RegisterQuicParsers(); + rs_websocket_register_parser(); rs_template_register_parser(); RegisterRFBParsers(); SCMqttRegisterParser(); diff --git a/src/app-layer-protos.c b/src/app-layer-protos.c index 368efacd88..b6e1b73d08 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 dc17ddca1e..5c27255a7b 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 0565f1f2c8..5608ae218f 100644 --- a/src/detect-engine-register.c +++ b/src/detect-engine-register.c @@ -241,6 +241,7 @@ #include "detect-quic-cyu-hash.h" #include "detect-quic-cyu-string.h" #include "detect-ja4-hash.h" +#include "detect-websocket.h" #include "detect-bypass.h" #include "detect-ftpdata.h" @@ -709,6 +710,7 @@ void SigTableSetup(void) DetectQuicCyuHashRegister(); DetectQuicCyuStringRegister(); DetectJa4HashRegister(); + DetectWebsocketRegister(); DetectBypassRegister(); DetectConfigRegister(); diff --git a/src/detect-engine-register.h b/src/detect-engine-register.h index 2eed5a2f48..cd2edf5979 100644 --- a/src/detect-engine-register.h +++ b/src/detect-engine-register.h @@ -319,6 +319,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_FLAGS, + DETECT_WEBSOCKET_PAYLOAD, DETECT_BYPASS, diff --git a/src/detect-websocket.c b/src/detect-websocket.c new file mode 100644 index 0000000000..91f650c85c --- /dev/null +++ b/src/detect-websocket.c @@ -0,0 +1,251 @@ +/* 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 void DetectWebSocketFlagsFree(DetectEngineCtx *de_ctx, void *de_ptr) +{ + rs_detect_u8_free(de_ptr); +} + +static int DetectWebSocketFlagsMatch(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 val = SCWebSocketGetFlags(txv); + return DetectU8Match(val, de); +} + +static int DetectWebSocketFlagsSetup(DetectEngineCtx *de_ctx, Signature *s, const char *rawstr) +{ + if (DetectSignatureSetAppProto(s, ALPROTO_WEBSOCKET) < 0) + return -1; + + DetectU8Data *de = SCWebSocketParseFlags(rawstr); + if (de == NULL) + return -1; + + if (SigMatchAppendSMToList( + de_ctx, s, DETECT_WEBSOCKET_FLAGS, (SigMatchCtx *)de, websocket_tx_id) == NULL) { + DetectWebSocketOpcodeFree(de_ctx, de); + 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_FLAGS].name = "websocket.flags"; + sigmatch_table[DETECT_WEBSOCKET_FLAGS].desc = "match WebSocket flags"; + sigmatch_table[DETECT_WEBSOCKET_FLAGS].url = "/rules/websocket-keywords.html#websocket-flags"; + sigmatch_table[DETECT_WEBSOCKET_FLAGS].AppLayerTxMatch = DetectWebSocketFlagsMatch; + sigmatch_table[DETECT_WEBSOCKET_FLAGS].Setup = DetectWebSocketFlagsSetup; + sigmatch_table[DETECT_WEBSOCKET_FLAGS].Free = DetectWebSocketFlagsFree; + + 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 0000000000..54e8a22ae4 --- /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.c b/src/output.c index 7441557f84..79524785a7 100644 --- a/src/output.c +++ b/src/output.c @@ -1083,6 +1083,10 @@ void OutputRegisterLoggers(void) JsonMQTTLogRegister(); /* Pgsql JSON logger. */ JsonPgsqlLogRegister(); + /* WebSocket JSON logger. */ + OutputRegisterTxSubModule(LOGGER_JSON_TX, "eve-log", "JsonWebSocketLog", "eve-log.websocket", + OutputJsonLogInitSub, ALPROTO_WEBSOCKET, JsonGenericDirPacketLogger, JsonLogThreadInit, + JsonLogThreadDeinit, NULL); /* Template JSON logger. */ OutputRegisterTxSubModule(LOGGER_JSON_TX, "eve-log", "JsonTemplateLog", "eve-log.template", OutputJsonLogInitSub, ALPROTO_TEMPLATE, JsonGenericDirPacketLogger, JsonLogThreadInit, @@ -1135,6 +1139,7 @@ static EveJsonSimpleAppLayerLogger simple_json_applayer_loggers[ALPROTO_MAX] = { { ALPROTO_MQTT, JsonMQTTAddMetadata }, { ALPROTO_PGSQL, JsonPgsqlAddMetadata }, { 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 31e4d1d3a0..0c83e29660 100644 --- a/suricata.yaml.in +++ b/suricata.yaml.in @@ -284,6 +284,7 @@ outputs: #md5: [body, subject] #- dnp3 + - websocket - ftp - rdp - nfs @@ -927,6 +928,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: