]> git.ipfire.org Git - thirdparty/suricata.git/commitdiff
app-layer: websockets protocol support
authorPhilippe Antoine <pantoine@oisf.net>
Fri, 22 Dec 2023 11:01:33 +0000 (12:01 +0100)
committerVictor Julien <victor@inliniac.net>
Wed, 17 Apr 2024 05:17:02 +0000 (07:17 +0200)
Ticket: 2695

23 files changed:
doc/userguide/rules/index.rst
doc/userguide/rules/intro.rst
doc/userguide/rules/websocket-keywords.rst [new file with mode: 0644]
etc/schema.json
rules/Makefile.am
rules/websocket-events.rules [new file with mode: 0644]
rust/src/lib.rs
rust/src/websocket/detect.rs [new file with mode: 0644]
rust/src/websocket/logger.rs [new file with mode: 0644]
rust/src/websocket/mod.rs [new file with mode: 0644]
rust/src/websocket/parser.rs [new file with mode: 0644]
rust/src/websocket/websocket.rs [new file with mode: 0644]
src/Makefile.am
src/app-layer-htp.c
src/app-layer-parser.c
src/app-layer-protos.c
src/app-layer-protos.h
src/detect-engine-register.c
src/detect-engine-register.h
src/detect-websocket.c [new file with mode: 0644]
src/detect-websocket.h [new file with mode: 0644]
src/output.c
suricata.yaml.in

index cf01e14d1f4b8470190fd837a81b9787f537209f..582520ebf9c11c0336c4ae77f66a58ce4182c2fa 100644 (file)
@@ -36,6 +36,7 @@ Suricata Rules
    quic-keywords
    nfs-keywords
    smtp-keywords
+   websocket-keywords
    app-layer
    xbits
    thresholding
index 9f8779625374b12c177eefd102d2c2f4aced1b17..41f7fe0b8327ce55b78b0c05b5115546fb079fef 100644 (file)
@@ -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 (file)
index 0000000..598bfe9
--- /dev/null
@@ -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 <rules-integer-keywords>`
+
+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 <rules-integer-keywords>`
+
+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 <rules-integer-keywords>`
+
+Examples::
+
+  websocket.opcode:1;
+  websocket.opcode:>8;
+  websocket.opcode:ping;
index 5fc3e8f8e66ab079ebe907f8c5acbeb763031925..451b1a6f6350676f2a6c96821f017a08841e4141 100644 (file)
                                 "tls": {
                                     "description": "Errors encountered parsing TLS protocol",
                                     "$ref": "#/$defs/stats_applayer_error"
+                                },
+                                "websocket": {
+                                    "$ref": "#/$defs/stats_applayer_error"
                                 }
                             },
                             "additionalProperties": false
                                 "tls": {
                                     "description": "Number of flows for TLS protocol",
                                     "type": "integer"
+                                },
+                                "websocket": {
+                                    "type": "integer"
                                 }
                             },
                             "additionalProperties": false
                                 },
                                 "tls": {
                                     "type": "integer"
+                                },
+                                "websocket": {
+                                    "type": "integer"
                                 }
                             },
                             "additionalProperties": false
                 }
             },
             "additionalProperties": false
+        },
+        "websocket": {
+            "type": "object",
+            "properties": {
+                "fin": {
+                    "type": "boolean"
+                },
+                "mask": {
+                    "type": "integer"
+                },
+                "opcode": {
+                    "type": "string"
+                }
+            },
+            "additionalProperties": false
         }
     },
     "$defs": {
index d0ea6eda622f7d9a822aabee13f4aa1c31fffe7a..cba0aa370af333aa3d4308992dec47c9af340013 100644 (file)
@@ -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 (file)
index 0000000..3acc211
--- /dev/null
@@ -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;)
index 9e58d4d805e8a22f64e536c4c047880a95d25aca..1dd16100ce30e23b1164e9436ade340fbc82fe8d 100644 (file)
@@ -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 (file)
index 0000000..44737ae
--- /dev/null
@@ -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<u8> {
+    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::<u8, WebSocketOpcode>(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<WebSocketFlag>> {
+    return many1(parse_flag_list_item)(s);
+}
+
+fn parse_flags(s: &str) -> Option<DetectUintData<u8>> {
+    // try first numerical value
+    if let Ok((_, ctx)) = detect_parse_uint::<u8>(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::<u8> {
+            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<u8> {
+    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 (file)
index 0000000..ab8b519
--- /dev/null
@@ -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 (file)
index 0000000..c57660f
--- /dev/null
@@ -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 (file)
index 0000000..edf6117
--- /dev/null
@@ -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<u32>,
+    pub payload: Vec<u8>,
+    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 (file)
index 0000000..4e94ea7
--- /dev/null
@@ -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<u8>,
+    compress: bool,
+}
+
+#[derive(Default)]
+pub struct WebSocketState {
+    state_data: AppLayerStateData,
+    tx_id: u64,
+    transactions: VecDeque<WebSocketTransaction>,
+
+    c2s_buf: WebSocketReassemblyBuffer,
+    s2c_buf: WebSocketReassemblyBuffer,
+
+    to_skip_tc: u64,
+    to_skip_ts: u64,
+}
+
+impl State<WebSocketTransaction> 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::<WebSocketState, WebSocketTransaction>,
+        ),
+        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::<u32>() {
+                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.");
+    }
+}
index d96e8a01385b84fe7c26fad33f2183f95705e069..7dd33a759911542a06705769c562d21f2dd593dd 100755 (executable)
@@ -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 \
index 94d4c38fbdbbe4c36d7f65ef2283964c12b397a1..a325f34c22a6bd662c883c65810c61ab5b2113d6 100644 (file)
@@ -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:
index b92562ef4962465a169a17d9c4e0615eff31bfb2..cf04151d79faf7763c2d386550abac6f172e4585 100644 (file)
@@ -1754,6 +1754,7 @@ void AppLayerParserRegisterProtocolParsers(void)
     RegisterSNMPParsers();
     RegisterSIPParsers();
     RegisterQuicParsers();
+    rs_websocket_register_parser();
     rs_template_register_parser();
     RegisterRFBParsers();
     SCMqttRegisterParser();
index 368efacd88d710ad7b12ce3e470df79fc8e2bbc4..b6e1b73d08d4abe4c2d937648efe6bbd5dd012db 100644 (file)
@@ -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" },
index dc17ddca1ea38e417900c80dd9be6e26c162219b..5c27255a7b46f46590ca2f25af268441aee640ba 100644 (file)
@@ -56,6 +56,7 @@ enum AppProtoEnum {
     ALPROTO_MQTT,
     ALPROTO_PGSQL,
     ALPROTO_TELNET,
+    ALPROTO_WEBSOCKET,
     ALPROTO_TEMPLATE,
     ALPROTO_RDP,
     ALPROTO_HTTP2,
index 0565f1f2c86df743d384497319000aa84e90eab8..5608ae218f51a9beeda71ab98160014c862ff80d 100644 (file)
 #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();
index 2eed5a2f483fd89d12e11c2130bac4f436bf92de..cd2edf5979b8ca6efd9d79bbfc0ee7c988e616fe 100644 (file)
@@ -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 (file)
index 0000000..91f650c
--- /dev/null
@@ -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 (file)
index 0000000..54e8a22
--- /dev/null
@@ -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__ */
index 7441557f84353488cd69759ef0844e667deba3d9..79524785a716f240a83f3fd137ffac432b07aed0 100644 (file)
@@ -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 },
index 31e4d1d3a029898bf1a18f54e3ff3ca154431bbf..0c83e29660700790685d6e8f389976eafca02f99 100644 (file)
@@ -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: