diff --git a/.github/workflows/builds.yml b/.github/workflows/builds.yml index 04a2f1d5102c..fab42b55ee19 100644 --- a/.github/workflows/builds.yml +++ b/.github/workflows/builds.yml @@ -186,6 +186,15 @@ jobs: cat eve.json | jq -c 'select(.dns)' test $(cat eve.json | jq -c 'select(.dns)' | wc -l) = "1" + - name: Test app-layer plugin + working-directory: examples/plugins/altemplate + run: | + RUSTFLAGS=-Clink-args=-Wl,-undefined,dynamic_lookup cargo build + ../../../src/suricata -S altemplate.rules --set plugins.0=./target/debug/libsuricata_altemplate.so --runmode=single -l . -c altemplate.yaml -k none -r ../../../rust/src/applayertemplate/template.pcap + cat eve.json | jq -c 'select(.altemplate)' + test $(cat eve.json | jq -c 'select(.altemplate)' | wc -l) = "3" + # we get 2 alerts and 1 altemplate events + - name: Test library build in tree working-directory: examples/lib/simple run: make clean all diff --git a/configure.ac b/configure.ac index ca964d9039a0..ede100c29249 100644 --- a/configure.ac +++ b/configure.ac @@ -2515,7 +2515,7 @@ AC_SUBST(enable_non_bundled_htp) AM_CONDITIONAL([BUILD_SHARED_LIBRARY], [test "x$enable_shared" = "xyes"] && [test "x$can_build_shared_library" = "xyes"]) -AC_CONFIG_FILES(Makefile src/Makefile rust/Makefile rust/Cargo.lock rust/Cargo.toml rust/derive/Cargo.toml rust/.cargo/config.toml) +AC_CONFIG_FILES(Makefile src/Makefile rust/Makefile rust/Cargo.lock rust/Cargo.toml rust/derive/Cargo.toml rust/plugin/Cargo.toml rust/.cargo/config.toml) AC_CONFIG_FILES(qa/Makefile qa/coccinelle/Makefile) AC_CONFIG_FILES(rules/Makefile doc/Makefile doc/userguide/Makefile) AC_CONFIG_FILES(contrib/Makefile contrib/file_processor/Makefile contrib/file_processor/Action/Makefile contrib/file_processor/Processor/Makefile) diff --git a/doc/userguide/devguide/libsuricata/index.rst b/doc/userguide/devguide/libsuricata/index.rst index d0c58c29b18b..cc176561558a 100644 --- a/doc/userguide/devguide/libsuricata/index.rst +++ b/doc/userguide/devguide/libsuricata/index.rst @@ -1,7 +1,7 @@ .. _libsuricata: -LibSuricata -=========== +LibSuricata and Plugins +======================= Using Suricata as a Library --------------------------- @@ -10,5 +10,50 @@ The ability to turn Suricata into a library that can be utilized in other tools is currently a work in progress, tracked by Redmine Ticket #2693: https://redmine.openinfosecfoundation.org/issues/2693. +Plugins +------- + A related work are Suricata plugins, also in progress and tracked by Redmine Ticket #4101: https://redmine.openinfosecfoundation.org/issues/4101. + +Plugins can be used by modifying suricata.yaml ``plugins`` section to include +the path of the dynamic library to load. + +Plugins should export a ``SCPluginRegister`` function that will be the entry point +used by Suricata. + +Application-layer plugins +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Application layer plugins can be added as demonstrated by example +https://github.com/OISF/suricata/blob/master/examples/plugins/altemplate/ + +The plugin code contains the same files as an application layer in the source tree: +- alname.rs +- detect.rs +- lib.rs +- log.rs +- parser.rs + +These files will have different ``use`` statements, targetting ``crate::suricata`` rather +than all the modules defined in Suricata itself. + +And the plugin contains also additional files: +- plugin.rs : defines the entry point of the plugin ``SCPluginRegister`` +- suricata.rs : something like a header-only definitions in Suricata needed by the plugin + +``SCPluginRegister`` should register callback that should then call ``SCPluginRegisterAppLayer`` +passing a ``SCAppLayerPlugin`` structure to suricata. + +This ``SCAppLayerPlugin`` begins by a version number ``SC_PLUGIN_API_VERSION`` for compatibility +between Suricata and the plugin. + +Known limitations are: + +- Plugins can only use simple logging as defined by ``EveJsonSimpleTxLogFunc`` + without suricata.yaml configuration, see https://github.com/OISF/suricata/pull/11160 +- Keywords cannot use validate callbacks, see https://redmine.openinfosecfoundation.org/issues/5634 +- Plugins cannot have keywords matching on mulitple protocols (like ja4), + see https://redmine.openinfosecfoundation.org/issues/7304 + +.. attention:: A pure rust pluging needs to be compiled with ``RUSTFLAGS=-Clink-args=-Wl,-undefined,dynamic_lookup`` \ No newline at end of file diff --git a/examples/plugins/README.md b/examples/plugins/README.md index 5300f750342b..869d7b3ea6be 100644 --- a/examples/plugins/README.md +++ b/examples/plugins/README.md @@ -9,3 +9,8 @@ is useful if you want to send EVE output to custom destinations. A minimal capture plugin that can be used as a template, but also used for testing capture plugin loading and registration in CI. + +## altemplate + +An app-layer template plugin with logging and detection. +Most code copied from rust/src/applayertemplate diff --git a/examples/plugins/altemplate/Cargo.toml b/examples/plugins/altemplate/Cargo.toml new file mode 100644 index 000000000000..f72c743f8e3b --- /dev/null +++ b/examples/plugins/altemplate/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "suricata-altemplate" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +nom7 = { version="7.0", package="nom" } +libc = "~0.2.82" +suricata-plugin = { path = "../../../rust/plugin" } + +[features] +default = ["suricata8"] +suricata8 = [] diff --git a/examples/plugins/altemplate/altemplate.rules b/examples/plugins/altemplate/altemplate.rules new file mode 100644 index 000000000000..458f546eece7 --- /dev/null +++ b/examples/plugins/altemplate/altemplate.rules @@ -0,0 +1,2 @@ +alert altemplate any any -> any any (msg:"TEST"; altemplate.buffer; content:"Hello"; flow:established,to_server; sid:1; rev:1;) +alert altemplate any any -> any any (msg:"TEST"; altemplate.buffer; content:"Bye"; flow:established,to_client; sid:2; rev:1;) diff --git a/examples/plugins/altemplate/altemplate.yaml b/examples/plugins/altemplate/altemplate.yaml new file mode 100644 index 000000000000..d41035b4660b --- /dev/null +++ b/examples/plugins/altemplate/altemplate.yaml @@ -0,0 +1,17 @@ +%YAML 1.1 +--- + +outputs: + - eve-log: + enabled: yes + types: + - altemplate + - alert + - flow + +app-layer: + protocols: + altemplate: + enabled: yes + detection-ports: + dp: 7000 diff --git a/examples/plugins/altemplate/src/detect.rs b/examples/plugins/altemplate/src/detect.rs new file mode 100644 index 000000000000..bcd89cff0d55 --- /dev/null +++ b/examples/plugins/altemplate/src/detect.rs @@ -0,0 +1,103 @@ +/* Copyright (C) 2024 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. + */ + +// same file as rust/src/applayertemplate/detect.rs except +// TEMPLATE_START_REMOVE removed +// different paths for use statements +// keywords prefixed with altemplate instead of just template + +use crate::suricata::{ + cast_pointer, DetectBufferSetActiveList, DetectHelperBufferMpmRegister, DetectHelperGetData, + DetectHelperKeywordRegister, DetectSignatureSetAppProto, Direction, SCSigTableElmt, +}; +use crate::template::{TemplateTransaction, ALPROTO_TEMPLATE}; +use std::os::raw::{c_int, c_void}; +use suricata_plugin::{SIGMATCH_INFO_STICKY_BUFFER, SIGMATCH_NOOPT}; + +static mut G_TEMPLATE_BUFFER_BUFFER_ID: c_int = 0; + +unsafe extern "C" fn template_buffer_setup( + de: *mut c_void, s: *mut c_void, _raw: *const std::os::raw::c_char, +) -> c_int { + if DetectSignatureSetAppProto(s, ALPROTO_TEMPLATE) != 0 { + return -1; + } + if DetectBufferSetActiveList(de, s, G_TEMPLATE_BUFFER_BUFFER_ID) < 0 { + return -1; + } + return 0; +} + +/// Get the request/response buffer for a transaction from C. +unsafe extern "C" fn template_buffer_get_data( + tx: *const c_void, flags: u8, buf: *mut *const u8, len: *mut u32, +) -> bool { + let tx = cast_pointer!(tx, TemplateTransaction); + if flags & Direction::ToClient as u8 != 0 { + if let Some(ref response) = tx.response { + *len = response.len() as u32; + *buf = response.as_ptr(); + return true; + } + } else if let Some(ref request) = tx.request { + *len = request.len() as u32; + *buf = request.as_ptr(); + return true; + } + return false; +} + +unsafe extern "C" fn template_buffer_get( + de: *mut c_void, transforms: *const c_void, flow: *const c_void, flow_flags: u8, + tx: *const c_void, list_id: c_int, +) -> *mut c_void { + return DetectHelperGetData( + de, + transforms, + flow, + flow_flags, + tx, + list_id, + template_buffer_get_data, + ); +} + +#[no_mangle] +pub unsafe extern "C" fn ScDetectTemplateRegister() { + // TODO create a suricata-verify test + // Setup a keyword structure and register it + let kw = SCSigTableElmt { + name: b"altemplate.buffer\0".as_ptr() as *const libc::c_char, + desc: b"Template content modifier to match on the template buffer\0".as_ptr() + as *const libc::c_char, + // TODO use the right anchor for url and write doc + url: b"/rules/template-keywords.html#buffer\0".as_ptr() as *const libc::c_char, + Setup: template_buffer_setup, + flags: SIGMATCH_NOOPT | SIGMATCH_INFO_STICKY_BUFFER, + AppLayerTxMatch: None, + Free: None, + }; + let _g_template_buffer_kw_id = DetectHelperKeywordRegister(&kw); + G_TEMPLATE_BUFFER_BUFFER_ID = DetectHelperBufferMpmRegister( + b"altemplate.buffer\0".as_ptr() as *const libc::c_char, + b"template.buffer intern description\0".as_ptr() as *const libc::c_char, + ALPROTO_TEMPLATE, + true, //toclient + true, //toserver + template_buffer_get, + ); +} diff --git a/examples/plugins/altemplate/src/lib.rs b/examples/plugins/altemplate/src/lib.rs new file mode 100644 index 000000000000..22df70a07652 --- /dev/null +++ b/examples/plugins/altemplate/src/lib.rs @@ -0,0 +1,6 @@ +mod detect; +mod log; +mod parser; +pub mod plugin; +mod suricata; +mod template; diff --git a/examples/plugins/altemplate/src/log.rs b/examples/plugins/altemplate/src/log.rs new file mode 100644 index 000000000000..3f0ae6a8b71f --- /dev/null +++ b/examples/plugins/altemplate/src/log.rs @@ -0,0 +1,46 @@ +/* Copyright (C) 2018 Open Information Security Foundation + * + * You can copy, redistribute or modify this Program under the terms of + * the GNU General Public License version 2 as published by the Free + * Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * version 2 along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +// same file as rust/src/applayertemplate/logger.rs except +// different paths for use statements +// open_object using altemplate instead of just template + +use crate::suricata::{cast_pointer, JsonBuilder, JsonError}; +use crate::template::TemplateTransaction; + +use std; + +fn log_template(tx: &TemplateTransaction, js: &mut JsonBuilder) -> Result<(), JsonError> { + js.open_object("altemplate")?; + if let Some(ref request) = tx.request { + js.set_string("request", request)?; + } + if let Some(ref response) = tx.response { + js.set_string("response", response)?; + } + js.close()?; + Ok(()) +} + +#[no_mangle] +pub unsafe extern "C" fn rs_template_logger_log( + tx: *const std::os::raw::c_void, js: *mut std::os::raw::c_void, +) -> bool { + let tx = cast_pointer!(tx, TemplateTransaction); + let js = cast_pointer!(js, JsonBuilder); + log_template(tx, js).is_ok() +} diff --git a/examples/plugins/altemplate/src/parser.rs b/examples/plugins/altemplate/src/parser.rs new file mode 100644 index 000000000000..1660cc16bbb4 --- /dev/null +++ b/examples/plugins/altemplate/src/parser.rs @@ -0,0 +1,66 @@ +/* Copyright (C) 2018 Open Information Security Foundation + * + * You can copy, redistribute or modify this Program under the terms of + * the GNU General Public License version 2 as published by the Free + * Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * version 2 along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +// same file as rust/src/applayertemplate/parser.rs except this comment + +use nom7::{ + bytes::streaming::{take, take_until}, + combinator::map_res, + IResult, +}; +use std; + +fn parse_len(input: &str) -> Result { + input.parse::() +} + +pub fn parse_message(i: &[u8]) -> IResult<&[u8], String> { + let (i, len) = map_res(map_res(take_until(":"), std::str::from_utf8), parse_len)(i)?; + let (i, _sep) = take(1_usize)(i)?; + let (i, msg) = map_res(take(len as usize), std::str::from_utf8)(i)?; + let result = msg.to_string(); + Ok((i, result)) +} + +#[cfg(test)] +mod tests { + use super::*; + use nom7::Err; + + /// Simple test of some valid data. + #[test] + fn test_parse_valid() { + let buf = b"12:Hello World!4:Bye."; + + let result = parse_message(buf); + match result { + Ok((remainder, message)) => { + // Check the first message. + assert_eq!(message, "Hello World!"); + + // And we should have 6 bytes left. + assert_eq!(remainder.len(), 6); + } + Err(Err::Incomplete(_)) => { + panic!("Result should not have been incomplete."); + } + Err(Err::Error(err)) | Err(Err::Failure(err)) => { + panic!("Result should not be an error: {:?}.", err); + } + } + } +} diff --git a/examples/plugins/altemplate/src/plugin.rs b/examples/plugins/altemplate/src/plugin.rs new file mode 100644 index 000000000000..7e47b20992cb --- /dev/null +++ b/examples/plugins/altemplate/src/plugin.rs @@ -0,0 +1,37 @@ +use super::suricata; +use super::template::rs_template_register_parser; +use crate::detect::ScDetectTemplateRegister; +use crate::log::rs_template_logger_log; +use crate::suricata::{SCAppLayerPlugin, SCLog, SCPlugin, SCPluginRegisterAppLayer}; + +extern "C" fn altemplate_plugin_init() { + SCLog!(suricata::Level::Notice, "Initializing altemplate plugin"); + let plugin = SCAppLayerPlugin { + version: 8, // api version for suricata compatibility + name: b"altemplate\0".as_ptr() as *const libc::c_char, + logname: b"JsonaltemplateLog\0".as_ptr() as *const libc::c_char, + confname: b"eve-log.altemplate\0".as_ptr() as *const libc::c_char, + Register: rs_template_register_parser, + Logger: rs_template_logger_log, + KeywordsRegister: ScDetectTemplateRegister, + }; + unsafe { + if SCPluginRegisterAppLayer(Box::into_raw(Box::new(plugin))) != 0 { + SCLog!( + suricata::Level::Error, + "Failed to register altemplate plugin" + ); + } + } +} + +#[no_mangle] +extern "C" fn SCPluginRegister() -> *const SCPlugin { + let plugin = SCPlugin { + name: b"altemplate\0".as_ptr() as *const libc::c_char, + license: b"MIT\0".as_ptr() as *const libc::c_char, + author: b"Philippe Antoine\0".as_ptr() as *const libc::c_char, + Init: altemplate_plugin_init, + }; + Box::into_raw(Box::new(plugin)) +} diff --git a/examples/plugins/altemplate/src/suricata.rs b/examples/plugins/altemplate/src/suricata.rs new file mode 100644 index 000000000000..2a66fcb0c02a --- /dev/null +++ b/examples/plugins/altemplate/src/suricata.rs @@ -0,0 +1,527 @@ +// This file is kind of the include required by API +// completed by helper functions + +use std::ffi::{CStr, CString}; +use std::os::raw::{c_char, c_int, c_void}; + +use suricata_plugin::AppProto; +//pub const STREAM_TOCLIENT: u8 = 0x08; + +// Opaque definitions +pub enum DetectEngineState {} +pub enum AppLayerDecoderEvents {} +pub enum Flow {} + +// Enum definitions +#[repr(C)] +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum Direction { + ToClient = 0x08, +} + +#[allow(dead_code)] +#[derive(Debug)] +#[repr(C)] +pub enum Level { + NotSet = -1, + None = 0, + + Error, + Warning, + Notice, + Info, + Perf, + Config, + Debug, +} + +// Struct definitions +#[repr(C)] +#[allow(non_snake_case)] +pub struct SCPlugin { + pub name: *const libc::c_char, + pub license: *const libc::c_char, + pub author: *const libc::c_char, + pub Init: extern "C" fn(), +} + +#[repr(C)] +#[allow(non_snake_case)] +pub struct SCAppLayerPlugin { + pub version: u64, + pub name: *const libc::c_char, + pub Register: unsafe extern "C" fn(), + pub KeywordsRegister: unsafe extern "C" fn(), + pub logname: *const libc::c_char, + pub confname: *const libc::c_char, + pub Logger: unsafe extern "C" fn( + tx: *const std::os::raw::c_void, + jb: *mut std::os::raw::c_void, + ) -> bool, +} + +#[repr(C)] +#[derive(Default, Debug, PartialEq, Eq)] +pub struct AppLayerTxConfig { + log_flags: u8, +} + +#[repr(C)] +#[derive(Default, Debug, PartialEq, Eq)] +pub struct LoggerFlags { + flags: u32, +} + +#[repr(C)] +#[derive(Debug, PartialEq, Eq)] +pub struct AppLayerTxData { + pub config: AppLayerTxConfig, + pub updated_tc: bool, + pub updated_ts: bool, + logged: LoggerFlags, + pub files_opened: u32, + pub files_logged: u32, + pub files_stored: u32, + + pub file_flags: u16, + pub file_tx: u8, + + pub guessed_applayer_logged: u8, + + detect_flags_ts: u64, + detect_flags_tc: u64, + + de_state: *mut DetectEngineState, + pub events: *mut AppLayerDecoderEvents, +} + +#[repr(C)] +#[derive(Default, Debug, PartialEq, Eq, Copy, Clone)] +pub struct AppLayerStateData { + pub file_flags: u16, +} + +#[repr(C)] +#[derive(Default, Debug, PartialEq, Eq, Copy, Clone)] +pub struct AppLayerResult { + pub status: i32, + pub consumed: u32, + pub needed: u32, +} + +#[repr(C)] +pub struct StreamSlice { + input: *const u8, + input_len: u32, + flags: u8, + offset: u64, +} + +#[repr(C)] +pub struct AppLayerGetTxIterTuple { + tx_ptr: *mut std::os::raw::c_void, + tx_id: u64, + has_next: bool, +} + +#[repr(C)] +#[derive(Debug)] +pub struct FileContainer { + pub head: *mut c_void, + pub tail: *mut c_void, +} + +#[repr(C)] +pub struct StreamingBufferConfig { + pub buf_size: u32, + + pub max_regions: u16, + pub region_gap: u32, + // do not bother with real prototypes + pub calloc: Option, + pub realloc: Option, + pub free: Option, +} + +#[allow(non_snake_case)] +#[repr(C)] +pub struct AppLayerGetFileState { + pub fc: *mut FileContainer, + pub cfg: *const StreamingBufferConfig, +} + +#[repr(C)] +#[allow(non_snake_case)] +pub struct SCSigTableElmt { + pub name: *const libc::c_char, + pub desc: *const libc::c_char, + pub url: *const libc::c_char, + pub flags: u16, + pub Setup: unsafe extern "C" fn( + de: *mut c_void, + s: *mut c_void, + raw: *const std::os::raw::c_char, + ) -> c_int, + pub Free: Option, + pub AppLayerTxMatch: Option< + unsafe extern "C" fn( + de: *mut c_void, + f: *mut c_void, + flags: u8, + state: *mut c_void, + tx: *mut c_void, + sig: *const c_void, + ctx: *const c_void, + ) -> c_int, + >, +} + +// Function type definitions for RustParser struct +pub type ParseFn = unsafe extern "C" fn( + flow: *const Flow, + state: *mut c_void, + pstate: *mut c_void, + stream_slice: StreamSlice, + data: *const c_void, +) -> AppLayerResult; +pub type ProbeFn = unsafe extern "C" fn( + flow: *const Flow, + flags: u8, + input: *const u8, + input_len: u32, + rdir: *mut u8, +) -> AppProto; +pub type StateAllocFn = extern "C" fn(*mut c_void, AppProto) -> *mut c_void; +pub type StateFreeFn = unsafe extern "C" fn(*mut c_void); +pub type StateTxFreeFn = unsafe extern "C" fn(*mut c_void, u64); +pub type StateGetTxFn = unsafe extern "C" fn(*mut c_void, u64) -> *mut c_void; +pub type StateGetTxCntFn = unsafe extern "C" fn(*mut c_void) -> u64; +pub type StateGetProgressFn = unsafe extern "C" fn(*mut c_void, u8) -> c_int; +pub type GetEventInfoFn = unsafe extern "C" fn(*const c_char, *mut c_int, *mut c_int) -> c_int; +pub type GetEventInfoByIdFn = unsafe extern "C" fn(c_int, *mut *const c_char, *mut c_int) -> i8; +pub type LocalStorageNewFn = extern "C" fn() -> *mut c_void; +pub type LocalStorageFreeFn = extern "C" fn(*mut c_void); +pub type GetTxFilesFn = unsafe extern "C" fn(*mut c_void, *mut c_void, u8) -> AppLayerGetFileState; +pub type GetTxIteratorFn = unsafe extern "C" fn( + ipproto: u8, + alproto: AppProto, + state: *mut c_void, + min_tx_id: u64, + max_tx_id: u64, + istate: &mut u64, +) -> AppLayerGetTxIterTuple; +pub type GetTxDataFn = unsafe extern "C" fn(*mut c_void) -> *mut AppLayerTxData; +pub type GetStateDataFn = unsafe extern "C" fn(*mut c_void) -> *mut AppLayerStateData; +pub type ApplyTxConfigFn = unsafe extern "C" fn(*mut c_void, *mut c_void, c_int, AppLayerTxConfig); +pub type GetFrameIdByName = unsafe extern "C" fn(*const c_char) -> c_int; +pub type GetFrameNameById = unsafe extern "C" fn(u8) -> *const c_char; + +#[repr(C)] +pub struct RustParser { + pub name: *const c_char, + pub default_port: *const c_char, + pub ipproto: u8, + pub probe_ts: Option, + pub probe_tc: Option, + pub min_depth: u16, + pub max_depth: u16, + pub state_new: StateAllocFn, + pub state_free: StateFreeFn, + pub parse_ts: ParseFn, + pub parse_tc: ParseFn, + pub get_tx_count: StateGetTxCntFn, + pub get_tx: StateGetTxFn, + pub tx_free: StateTxFreeFn, + + pub tx_comp_st_ts: c_int, + pub tx_comp_st_tc: c_int, + pub tx_get_progress: StateGetProgressFn, + + pub get_eventinfo: Option, + pub get_eventinfo_byid: Option, + pub localstorage_new: Option, + pub localstorage_free: Option, + + pub get_tx_files: Option, + + pub get_tx_iterator: Option, + + pub get_state_data: GetStateDataFn, + + pub get_tx_data: GetTxDataFn, + pub apply_tx_config: Option, + pub flags: u32, + pub get_frame_id_by_name: Option, + pub get_frame_name_by_id: Option, +} + +// Suricata functions to use +extern "C" { + pub fn ConfGet(key: *const c_char, res: *mut *const c_char) -> i8; + pub fn SCLogMessage( + level: c_int, filename: *const std::os::raw::c_char, line: std::os::raw::c_uint, + function: *const std::os::raw::c_char, subsystem: *const std::os::raw::c_char, + message: *const std::os::raw::c_char, + ) -> c_int; + + pub fn AppLayerParserStateIssetFlag(state: *mut c_void, flag: u16) -> u16; + + pub fn AppLayerProtoDetectConfProtoDetectionEnabled( + ipproto: *const c_char, proto: *const c_char, + ) -> c_int; + + pub fn AppLayerRegisterProtocolDetection( + parser: *const RustParser, enable_default: c_int, + ) -> AppProto; + pub fn AppLayerParserConfParserEnabled(ipproto: *const c_char, proto: *const c_char) -> c_int; + pub fn AppLayerRegisterParser(parser: *const RustParser, alproto: AppProto) -> c_int; + pub fn SCPluginRegisterAppLayer(plugin: *const SCAppLayerPlugin) -> c_int; + pub fn AppLayerDecoderEventsSetEventRaw(events: *mut *mut AppLayerDecoderEvents, event: u8); + pub fn AppLayerParserRegisterLogger(pproto: u8, alproto: AppProto); + pub fn DetectHelperBufferMpmRegister( + name: *const c_char, desc: *const c_char, alproto: AppProto, toclient: bool, + toserver: bool, + get_data: unsafe extern "C" fn( + *mut c_void, + *const c_void, + *const c_void, + u8, + *const c_void, + i32, + ) -> *mut c_void, + ) -> c_int; + pub fn DetectHelperGetData( + de: *mut c_void, transforms: *const c_void, flow: *const c_void, flow_flags: u8, + tx: *const c_void, list_id: c_int, + get_buf: unsafe extern "C" fn(*const c_void, u8, *mut *const u8, *mut u32) -> bool, + ) -> *mut c_void; + pub fn DetectHelperKeywordRegister(kw: *const SCSigTableElmt) -> c_int; + pub fn DetectSignatureSetAppProto(s: *mut c_void, alproto: AppProto) -> c_int; + pub fn DetectBufferSetActiveList(de: *mut c_void, s: *mut c_void, bufid: c_int) -> c_int; +} + +// Helper implementations to feel like usual + +// Jsonbuilder opaque with implementation using C API to feel like usual +pub enum JsonBuilder {} + +impl JsonBuilder { + pub fn close(&mut self) -> Result<(), JsonError> { + if unsafe { !jb_close(self) } { + return Err(JsonError::SuricataError); + } + Ok(()) + } + pub fn open_object(&mut self, key: &str) -> Result<(), JsonError> { + let keyc = CString::new(key).unwrap(); + if unsafe { !jb_open_object(self, keyc.as_ptr()) } { + return Err(JsonError::SuricataError); + } + Ok(()) + } + pub fn set_string(&mut self, key: &str, val: &str) -> Result<(), JsonError> { + let keyc = CString::new(key).unwrap(); + let valc = CString::new(val.escape_default().to_string()).unwrap(); + if unsafe { !jb_set_string(self, keyc.as_ptr(), valc.as_ptr()) } { + return Err(JsonError::SuricataError); + } + Ok(()) + } +} + +#[derive(Debug, PartialEq, Eq)] +pub enum JsonError { + SuricataError, +} + +extern "C" { + pub fn jb_set_string(jb: &mut JsonBuilder, key: *const c_char, val: *const c_char) -> bool; + pub fn jb_close(jb: &mut JsonBuilder) -> bool; + pub fn jb_open_object(jb: &mut JsonBuilder, key: *const c_char) -> bool; +} + +// Helper functions +impl AppLayerResult { + pub fn ok() -> Self { + Default::default() + } + pub fn err() -> Self { + Self { + status: -1, + ..Default::default() + } + } + pub fn incomplete(consumed: u32, needed: u32) -> Self { + Self { + status: 1, + consumed, + needed, + } + } +} + +impl StreamSlice { + pub fn as_slice(&self) -> &[u8] { + if self.input.is_null() && self.input_len == 0 { + unsafe { + return std::slice::from_raw_parts( + std::ptr::NonNull::::dangling().as_ptr(), + self.input_len as usize, + ); + } + } + unsafe { std::slice::from_raw_parts(self.input, self.input_len as usize) } + } + pub fn is_gap(&self) -> bool { + self.input.is_null() && self.input_len > 0 + } + pub fn gap_size(&self) -> u32 { + self.input_len + } +} + +impl AppLayerGetTxIterTuple { + pub fn with_values( + tx_ptr: *mut std::os::raw::c_void, tx_id: u64, has_next: bool, + ) -> AppLayerGetTxIterTuple { + AppLayerGetTxIterTuple { + tx_ptr, + tx_id, + has_next, + } + } + pub fn not_found() -> AppLayerGetTxIterTuple { + AppLayerGetTxIterTuple { + tx_ptr: std::ptr::null_mut(), + tx_id: 0, + has_next: false, + } + } +} + +impl AppLayerTxData { + pub fn new() -> Self { + Self { + config: AppLayerTxConfig::default(), + logged: LoggerFlags::default(), + files_opened: 0, + files_logged: 0, + files_stored: 0, + file_flags: 0, + file_tx: 0, + guessed_applayer_logged: 0, + updated_tc: true, + updated_ts: true, + detect_flags_ts: 0, + detect_flags_tc: 0, + de_state: std::ptr::null_mut(), + events: std::ptr::null_mut(), + } + } + pub fn set_event(&mut self, event: u8) { + unsafe { + AppLayerDecoderEventsSetEventRaw(&mut self.events, event); + } + } +} + +// Helper functions to feel like usual + +// Return the string value of a configuration value. +pub fn conf_get(key: &str) -> Option<&str> { + let mut vptr: *const c_char = std::ptr::null_mut(); + + unsafe { + let s = CString::new(key).unwrap(); + if ConfGet(s.as_ptr(), &mut vptr) != 1 { + return None; + } + } + + if vptr.is_null() { + return None; + } + + let value = std::str::from_utf8(unsafe { CStr::from_ptr(vptr).to_bytes() }).unwrap(); + + return Some(value); +} + +// Macro definitions + +macro_rules! cast_pointer { + ($ptr:ident, $ty:ty) => { + &mut *($ptr as *mut $ty) + }; +} +pub(crate) use cast_pointer; + +// This macro returns the function name. +// +// This macro has been borrowed from https://github.com/popzxc/stdext-rs, which +// is released under the MIT license as there is currently no macro in Rust +// to provide the function name. +macro_rules! function { + () => {{ + // Okay, this is ugly, I get it. However, this is the best we can get on a stable rust. + fn __f() {} + fn type_name_of(_: T) -> &'static str { + std::any::type_name::() + } + let name = type_name_of(__f); + &name[..name.len() - 5] + }}; +} +pub(crate) use function; + +macro_rules!SCLog { + ($level:expr, $($arg:tt)*) => { + $crate::suricata::sclog($level, file!(), line!(), crate::suricata::function!(), + &(format!($($arg)*))); + } +} + +pub(crate) use SCLog; + +pub fn sclog(level: Level, filename: &str, line: u32, function: &str, message: &str) { + let filenamec = CString::new(filename).unwrap(); + let functionc = CString::new(function).unwrap(); + let modulec = CString::new("altemplate").unwrap(); + let messagec = CString::new(message).unwrap(); + unsafe { + SCLogMessage( + level as i32, + filenamec.as_ptr(), + line, + (functionc).as_ptr(), + (modulec).as_ptr(), + (messagec).as_ptr(), + ); + } +} + +#[macro_export] +macro_rules!SCLogNotice { + ($($arg:tt)*) => { + $crate::suricata::sclog(Level::Notice, file!(), line!(), crate::suricata::function!(), + &(format!($($arg)*))); + } +} +pub(crate) use SCLogNotice; + +#[macro_export] +macro_rules!SCLogError { + ($($arg:tt)*) => { + $crate::suricata::sclog(Level::Error, file!(), line!(), crate::suricata::function!(), + &(format!($($arg)*))); + } +} +pub(crate) use SCLogError; + +#[macro_export] +macro_rules! build_slice { + ($buf:ident, $len:expr) => { + std::slice::from_raw_parts($buf, $len) + }; +} +pub(crate) use build_slice; diff --git a/examples/plugins/altemplate/src/template.rs b/examples/plugins/altemplate/src/template.rs new file mode 100644 index 000000000000..76bd5030361a --- /dev/null +++ b/examples/plugins/altemplate/src/template.rs @@ -0,0 +1,583 @@ +/* Copyright (C) 2018-2022 Open Information Security Foundation + * + * You can copy, redistribute or modify this Program under the terms of + * the GNU General Public License version 2 as published by the Free + * Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * version 2 along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +// same file as rust/src/applayertemplate/template.rs except +// different paths for use statements +// recoding of derive(AppLayerEvent) +// recoding of state_get_tx_iterator +// recoding of export_state_data_get +// remove TEMPLATE_START_REMOVE +// name is altemplate instead of template + +use super::parser; +use crate::suricata::{ + build_slice, cast_pointer, conf_get, AppLayerGetTxIterTuple, AppLayerParserConfParserEnabled, + AppLayerParserRegisterLogger, AppLayerParserStateIssetFlag, + AppLayerProtoDetectConfProtoDetectionEnabled, AppLayerRegisterParser, + AppLayerRegisterProtocolDetection, AppLayerResult, AppLayerStateData, AppLayerTxData, Flow, + Level, RustParser, SCLogError, SCLogNotice, StreamSlice, +}; +use nom7 as nom; +use std; +use std::collections::VecDeque; +use std::ffi::{CStr, CString}; +use std::os::raw::{c_char, c_int, c_void}; +use suricata_plugin::{ + AppProto, ALPROTO_UNKNOWN, APP_LAYER_EVENT_TYPE_TRANSACTION, APP_LAYER_PARSER_EOF_TC, + APP_LAYER_PARSER_EOF_TS, APP_LAYER_PARSER_OPT_ACCEPT_GAPS, IPPROTO_TCP, +}; + +static mut TEMPLATE_MAX_TX: usize = 256; + +pub(super) static mut ALPROTO_TEMPLATE: AppProto = ALPROTO_UNKNOWN; + +enum TemplateEvent { + TooManyTransactions, +} + +impl TemplateEvent { + fn from_id(id: i32) -> Option { + match id { + 0 => Some(TemplateEvent::TooManyTransactions), + _ => None, + } + } + + fn to_cstring(&self) -> &str { + match *self { + TemplateEvent::TooManyTransactions => "too_many_transactions\0", + } + } + + fn as_i32(&self) -> i32 { + match *self { + TemplateEvent::TooManyTransactions => 0, + } + } + + fn from_string(s: &str) -> Option { + match s { + "too_many_transactions" => Some(TemplateEvent::TooManyTransactions), + _ => None, + } + } + + pub unsafe extern "C" fn get_event_info( + event_name: *const std::os::raw::c_char, event_id: *mut std::os::raw::c_int, + event_type: *mut std::os::raw::c_int, + ) -> std::os::raw::c_int { + if event_name.is_null() { + return -1; + } + + let event = match CStr::from_ptr(event_name) + .to_str() + .map(TemplateEvent::from_string) + { + Ok(Some(event)) => event.as_i32(), + _ => { + return -1; + } + }; + *event_type = APP_LAYER_EVENT_TYPE_TRANSACTION as std::os::raw::c_int; + *event_id = event as std::os::raw::c_int; + 0 + } + + pub unsafe extern "C" fn get_event_info_by_id( + event_id: std::os::raw::c_int, event_name: *mut *const std::os::raw::c_char, + event_type: *mut std::os::raw::c_int, + ) -> i8 { + if let Some(e) = TemplateEvent::from_id(event_id) { + *event_name = e.to_cstring().as_ptr() as *const std::os::raw::c_char; + *event_type = APP_LAYER_EVENT_TYPE_TRANSACTION as std::os::raw::c_int; + return 0; + } + -1 + } +} + +pub struct TemplateTransaction { + tx_id: u64, + pub request: Option, + pub response: Option, + + tx_data: AppLayerTxData, +} + +impl Default for TemplateTransaction { + fn default() -> Self { + Self::new() + } +} + +impl TemplateTransaction { + pub fn new() -> TemplateTransaction { + Self { + tx_id: 0, + request: None, + response: None, + tx_data: AppLayerTxData::new(), + } + } +} + +#[derive(Default)] +pub struct TemplateState { + state_data: AppLayerStateData, + tx_id: u64, + transactions: VecDeque, + request_gap: bool, + response_gap: bool, +} + +impl TemplateState { + 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<&TemplateTransaction> { + self.transactions.iter().find(|tx| tx.tx_id == tx_id + 1) + } + + fn new_tx(&mut self) -> TemplateTransaction { + let mut tx = TemplateTransaction::new(); + self.tx_id += 1; + tx.tx_id = self.tx_id; + return tx; + } + + fn find_request(&mut self) -> Option<&mut TemplateTransaction> { + self.transactions + .iter_mut() + .find(|tx| tx.response.is_none()) + } + + fn parse_request(&mut self, input: &[u8]) -> AppLayerResult { + // We're not interested in empty requests. + if input.is_empty() { + return AppLayerResult::ok(); + } + + // If there was gap, check we can sync up again. + if self.request_gap { + if probe(input).is_err() { + // The parser now needs to decide what to do as we are not in sync. + // For this template, we'll just try again next time. + return AppLayerResult::ok(); + } + + // It looks like we're in sync with a message header, clear gap + // state and keep parsing. + self.request_gap = false; + } + + let mut start = input; + while !start.is_empty() { + match parser::parse_message(start) { + Ok((rem, request)) => { + start = rem; + + SCLogNotice!("Request: {}", request); + let mut tx = self.new_tx(); + tx.request = Some(request); + if self.transactions.len() >= unsafe { TEMPLATE_MAX_TX } { + tx.tx_data + .set_event(TemplateEvent::TooManyTransactions as u8); + } + self.transactions.push_back(tx); + if self.transactions.len() >= unsafe { TEMPLATE_MAX_TX } { + return AppLayerResult::err(); + } + } + Err(nom::Err::Incomplete(_)) => { + // Not enough data. This parser doesn't give us a good indication + // of how much data is missing so just ask for one more byte so the + // parse is called as soon as more data is received. + let consumed = input.len() - start.len(); + let needed = start.len() + 1; + return AppLayerResult::incomplete(consumed as u32, needed as u32); + } + Err(_) => { + return AppLayerResult::err(); + } + } + } + + // Input was fully consumed. + return AppLayerResult::ok(); + } + + fn parse_response(&mut self, input: &[u8]) -> AppLayerResult { + // We're not interested in empty responses. + if input.is_empty() { + return AppLayerResult::ok(); + } + + if self.response_gap { + if probe(input).is_err() { + // The parser now needs to decide what to do as we are not in sync. + // For this template, we'll just try again next time. + return AppLayerResult::ok(); + } + + // It looks like we're in sync with a message header, clear gap + // state and keep parsing. + self.response_gap = false; + } + let mut start = input; + while !start.is_empty() { + match parser::parse_message(start) { + Ok((rem, response)) => { + start = rem; + + if let Some(tx) = self.find_request() { + tx.tx_data.updated_tc = true; + tx.response = Some(response); + SCLogNotice!("Found response for request:"); + SCLogNotice!("- Request: {:?}", tx.request); + SCLogNotice!("- Response: {:?}", tx.response); + } + } + Err(nom::Err::Incomplete(_)) => { + let consumed = input.len() - start.len(); + let needed = start.len() + 1; + return AppLayerResult::incomplete(consumed as u32, needed as u32); + } + Err(_) => { + return AppLayerResult::err(); + } + } + } + + // All input was fully consumed. + return AppLayerResult::ok(); + } + + fn on_request_gap(&mut self, _size: u32) { + self.request_gap = true; + } + + fn on_response_gap(&mut self, _size: u32) { + self.response_gap = true; + } +} + +/// Probe for a valid header. +/// +/// As this template protocol uses messages prefixed with the size +/// as a string followed by a ':', we look at up to the first 10 +/// characters for that pattern. +fn probe(input: &[u8]) -> nom::IResult<&[u8], ()> { + let size = std::cmp::min(10, input.len()); + let (rem, prefix) = nom::bytes::complete::take(size)(input)?; + nom::sequence::terminated( + nom::bytes::complete::take_while1(nom::character::is_digit), + nom::bytes::complete::tag(":"), + )(prefix)?; + Ok((rem, ())) +} + +// C exports. + +/// C entry point for a probing parser. +unsafe extern "C" fn rs_template_probing_parser( + _flow: *const Flow, _direction: u8, input: *const u8, input_len: u32, _rdir: *mut u8, +) -> AppProto { + // Need at least 2 bytes. + if input_len > 1 && !input.is_null() { + let slice = build_slice!(input, input_len as usize); + if probe(slice).is_ok() { + return ALPROTO_TEMPLATE; + } + } + return ALPROTO_UNKNOWN; +} + +extern "C" fn rs_template_state_new( + _orig_state: *mut c_void, _orig_proto: AppProto, +) -> *mut c_void { + let state = TemplateState::new(); + let boxed = Box::new(state); + return Box::into_raw(boxed) as *mut c_void; +} + +unsafe extern "C" fn rs_template_state_free(state: *mut c_void) { + std::mem::drop(Box::from_raw(state as *mut TemplateState)); +} + +unsafe extern "C" fn rs_template_state_tx_free(state: *mut c_void, tx_id: u64) { + let state = cast_pointer!(state, TemplateState); + state.free_tx(tx_id); +} + +unsafe extern "C" fn rs_template_parse_request( + _flow: *const Flow, state: *mut c_void, pstate: *mut c_void, stream_slice: StreamSlice, + _data: *const c_void, +) -> AppLayerResult { + let eof = AppLayerParserStateIssetFlag(pstate, APP_LAYER_PARSER_EOF_TS) > 0; + + if eof { + // If needed, handle EOF, or pass it into the parser. + return AppLayerResult::ok(); + } + + let state = cast_pointer!(state, TemplateState); + + if stream_slice.is_gap() { + // Here we have a gap signaled by the input being null, but a greater + // than 0 input_len which provides the size of the gap. + state.on_request_gap(stream_slice.gap_size()); + AppLayerResult::ok() + } else { + let buf = stream_slice.as_slice(); + state.parse_request(buf) + } +} + +unsafe extern "C" fn rs_template_parse_response( + _flow: *const Flow, state: *mut c_void, pstate: *mut c_void, stream_slice: StreamSlice, + _data: *const c_void, +) -> AppLayerResult { + let _eof = AppLayerParserStateIssetFlag(pstate, APP_LAYER_PARSER_EOF_TC) > 0; + let state = cast_pointer!(state, TemplateState); + + if stream_slice.is_gap() { + // Here we have a gap signaled by the input being null, but a greater + // than 0 input_len which provides the size of the gap. + state.on_response_gap(stream_slice.gap_size()); + AppLayerResult::ok() + } else { + let buf = stream_slice.as_slice(); + state.parse_response(buf) + } +} + +unsafe extern "C" fn rs_template_state_get_tx(state: *mut c_void, tx_id: u64) -> *mut c_void { + let state = cast_pointer!(state, TemplateState); + 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_template_state_get_tx_count(state: *mut c_void) -> u64 { + let state = cast_pointer!(state, TemplateState); + return state.tx_id; +} + +unsafe extern "C" fn rs_template_tx_get_alstate_progress(tx: *mut c_void, _direction: u8) -> c_int { + let tx = cast_pointer!(tx, TemplateTransaction); + + // Transaction is done if we have a response. + if tx.response.is_some() { + return 1; + } + return 0; +} + +#[no_mangle] +pub unsafe extern "C" fn rs_template_get_tx_data( + tx: *mut std::os::raw::c_void, +) -> *mut AppLayerTxData { + let tx = &mut *(tx as *mut TemplateTransaction); + &mut tx.tx_data +} + +#[no_mangle] +pub unsafe extern "C" fn rs_template_get_state_data( + state: *mut std::os::raw::c_void, +) -> *mut AppLayerStateData { + let state = &mut *(state as *mut TemplateState); + &mut state.state_data +} + +pub unsafe extern "C" fn template_get_tx_iterator( + _ipproto: u8, _alproto: AppProto, state: *mut std::os::raw::c_void, min_tx_id: u64, + _max_tx_id: u64, istate: &mut u64, +) -> AppLayerGetTxIterTuple { + let state = cast_pointer!(state, TemplateState); + let mut index = *istate as usize; + let len = state.transactions.len(); + while index < len { + let tx = state.transactions.get(index).unwrap(); + if tx.tx_id < min_tx_id + 1 { + index += 1; + continue; + } + *istate = index as u64; + return AppLayerGetTxIterTuple::with_values( + tx as *const _ as *mut _, + tx.tx_id - 1, + len - index > 1, + ); + } + AppLayerGetTxIterTuple::not_found() +} + +// Parser name as a C style string. +const PARSER_NAME: &[u8] = b"altemplate\0"; + +#[no_mangle] +pub unsafe extern "C" fn rs_template_register_parser() { + let default_port = CString::new("[7000]").unwrap(); + let parser = RustParser { + name: PARSER_NAME.as_ptr() as *const c_char, + default_port: default_port.as_ptr(), + ipproto: IPPROTO_TCP, + probe_ts: Some(rs_template_probing_parser), + probe_tc: Some(rs_template_probing_parser), + min_depth: 0, + max_depth: 16, + state_new: rs_template_state_new, + state_free: rs_template_state_free, + tx_free: rs_template_state_tx_free, + parse_ts: rs_template_parse_request, + parse_tc: rs_template_parse_response, + get_tx_count: rs_template_state_get_tx_count, + get_tx: rs_template_state_get_tx, + tx_comp_st_ts: 1, + tx_comp_st_tc: 1, + tx_get_progress: rs_template_tx_get_alstate_progress, + get_eventinfo: Some(TemplateEvent::get_event_info), + get_eventinfo_byid: Some(TemplateEvent::get_event_info_by_id), + localstorage_new: None, + localstorage_free: None, + get_tx_files: None, + get_tx_iterator: Some(template_get_tx_iterator), + get_tx_data: rs_template_get_tx_data, + get_state_data: rs_template_get_state_data, + apply_tx_config: None, + flags: APP_LAYER_PARSER_OPT_ACCEPT_GAPS, + get_frame_id_by_name: None, + get_frame_name_by_id: None, + }; + + let ip_proto_str = CString::new("tcp").unwrap(); + + if AppLayerProtoDetectConfProtoDetectionEnabled(ip_proto_str.as_ptr(), parser.name) != 0 { + let alproto = AppLayerRegisterProtocolDetection(&parser, 1); + ALPROTO_TEMPLATE = alproto; + if AppLayerParserConfParserEnabled(ip_proto_str.as_ptr(), parser.name) != 0 { + let _ = AppLayerRegisterParser(&parser, alproto); + } + if let Some(val) = conf_get("app-layer.protocols.template.max-tx") { + if let Ok(v) = val.parse::() { + TEMPLATE_MAX_TX = v; + } else { + SCLogError!("Invalid value for template.max-tx"); + } + } + AppLayerParserRegisterLogger(IPPROTO_TCP, ALPROTO_TEMPLATE); + SCLogNotice!("Rust template parser registered."); + } else { + SCLogNotice!("Protocol detector and parser disabled for TEMPLATE."); + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_probe() { + assert!(probe(b"1").is_err()); + assert!(probe(b"1:").is_ok()); + assert!(probe(b"123456789:").is_ok()); + assert!(probe(b"0123456789:").is_err()); + } + + #[test] + fn test_incomplete() { + let mut state = TemplateState::new(); + let buf = b"5:Hello3:bye"; + + let r = state.parse_request(&buf[0..0]); + assert_eq!( + r, + AppLayerResult { + status: 0, + consumed: 0, + needed: 0 + } + ); + + let r = state.parse_request(&buf[0..1]); + assert_eq!( + r, + AppLayerResult { + status: 1, + consumed: 0, + needed: 2 + } + ); + + let r = state.parse_request(&buf[0..2]); + assert_eq!( + r, + AppLayerResult { + status: 1, + consumed: 0, + needed: 3 + } + ); + + // This is the first message and only the first message. + let r = state.parse_request(&buf[0..7]); + assert_eq!( + r, + AppLayerResult { + status: 0, + consumed: 0, + needed: 0 + } + ); + + // The first message and a portion of the second. + let r = state.parse_request(&buf[0..9]); + assert_eq!( + r, + AppLayerResult { + status: 1, + consumed: 7, + needed: 3 + } + ); + } +} diff --git a/rust/Cargo.toml.in b/rust/Cargo.toml.in index 8fad5fee77e3..aa9129dc8095 100644 --- a/rust/Cargo.toml.in +++ b/rust/Cargo.toml.in @@ -7,7 +7,7 @@ edition = "2021" rust-version = "1.67.1" [workspace] -members = [".", "./derive"] +members = [".", "./derive", "./plugin"] [lib] crate-type = ["staticlib", "rlib"] diff --git a/rust/Makefile.am b/rust/Makefile.am index d53eb97090e1..2120db83e1cc 100644 --- a/rust/Makefile.am +++ b/rust/Makefile.am @@ -1,9 +1,10 @@ -EXTRA_DIST = src derive \ +EXTRA_DIST = src derive plugin \ .cargo/config.toml.in \ cbindgen.toml \ dist/rust-bindings.h \ vendor \ Cargo.toml Cargo.lock \ + plugin/Cargo.toml \ derive/Cargo.toml if !DEBUG diff --git a/rust/plugin/Cargo.toml.in b/rust/plugin/Cargo.toml.in new file mode 100644 index 000000000000..7b2e1a4974e2 --- /dev/null +++ b/rust/plugin/Cargo.toml.in @@ -0,0 +1,9 @@ +[package] +name = "suricata-plugin" +version = "@PACKAGE_VERSION@" +license = "GPL-2.0-only" +description = "Re-exports for Suricata plugins" +edition = "2021" + +[dependencies] +suricata = { path = "../", version = "@PACKAGE_VERSION@" } diff --git a/rust/plugin/src/lib.rs b/rust/plugin/src/lib.rs new file mode 100644 index 000000000000..e7cb389aea1b --- /dev/null +++ b/rust/plugin/src/lib.rs @@ -0,0 +1,35 @@ +/* Copyright (C) 2020-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 suricata::*; + +// Type definitions +pub type AppProto = core::AppProto; +pub type AppLayerEventType = core::AppLayerEventType; + +// Constant definitions +pub const ALPROTO_UNKNOWN: AppProto = core::ALPROTO_UNKNOWN; +pub const IPPROTO_TCP : u8 = core::IPPROTO_TCP; + +pub const APP_LAYER_PARSER_OPT_ACCEPT_GAPS : u32 = applayer::APP_LAYER_PARSER_OPT_ACCEPT_GAPS; +pub const APP_LAYER_PARSER_EOF_TC : u16 = applayer::APP_LAYER_PARSER_EOF_TC; +pub const APP_LAYER_PARSER_EOF_TS : u16 = applayer::APP_LAYER_PARSER_EOF_TS; + +pub const APP_LAYER_EVENT_TYPE_TRANSACTION : AppLayerEventType = AppLayerEventType::APP_LAYER_EVENT_TYPE_TRANSACTION; + +pub const SIGMATCH_NOOPT: u16 = detect::SIGMATCH_NOOPT; +pub const SIGMATCH_INFO_STICKY_BUFFER: u16 = detect::SIGMATCH_INFO_STICKY_BUFFER; \ No newline at end of file diff --git a/rust/src/applayertemplate/logger.rs b/rust/src/applayertemplate/logger.rs index 766a07acdb9d..a970e832776e 100644 --- a/rust/src/applayertemplate/logger.rs +++ b/rust/src/applayertemplate/logger.rs @@ -33,8 +33,9 @@ fn log_template(tx: &TemplateTransaction, js: &mut JsonBuilder) -> Result<(), Js #[no_mangle] pub unsafe extern "C" fn rs_template_logger_log( - tx: *mut std::os::raw::c_void, js: &mut JsonBuilder, + tx: *const std::os::raw::c_void, js: *mut std::os::raw::c_void, ) -> bool { let tx = cast_pointer!(tx, TemplateTransaction); + let js = cast_pointer!(js, JsonBuilder); log_template(tx, js).is_ok() } diff --git a/rust/src/applayertemplate/template.rs b/rust/src/applayertemplate/template.rs index 9a23ee683f5e..99972f413bbd 100644 --- a/rust/src/applayertemplate/template.rs +++ b/rust/src/applayertemplate/template.rs @@ -120,7 +120,9 @@ impl TemplateState { } fn find_request(&mut self) -> Option<&mut TemplateTransaction> { - self.transactions.iter_mut().find(|tx| tx.response.is_none()) + self.transactions + .iter_mut() + .find(|tx| tx.response.is_none()) } fn parse_request(&mut self, input: &[u8]) -> AppLayerResult { @@ -151,11 +153,12 @@ impl TemplateState { SCLogNotice!("Request: {}", request); let mut tx = self.new_tx(); tx.request = Some(request); - if self.transactions.len() >= unsafe {TEMPLATE_MAX_TX} { - tx.tx_data.set_event(TemplateEvent::TooManyTransactions as u8); + if self.transactions.len() >= unsafe { TEMPLATE_MAX_TX } { + tx.tx_data + .set_event(TemplateEvent::TooManyTransactions as u8); } self.transactions.push_back(tx); - if self.transactions.len() >= unsafe {TEMPLATE_MAX_TX} { + if self.transactions.len() >= unsafe { TEMPLATE_MAX_TX } { return AppLayerResult::err(); } } @@ -200,7 +203,7 @@ impl TemplateState { Ok((rem, response)) => { start = rem; - if let Some(tx) = self.find_request() { + if let Some(tx) = self.find_request() { tx.tx_data.updated_tc = true; tx.response = Some(response); SCLogNotice!("Found response for request:"); diff --git a/rust/src/detect/mod.rs b/rust/src/detect/mod.rs index c00f0dfdeb18..de82a1559d91 100644 --- a/rust/src/detect/mod.rs +++ b/rust/src/detect/mod.rs @@ -76,9 +76,9 @@ pub struct SCSigTableElmt { >, } -pub(crate) const SIGMATCH_NOOPT: u16 = 1; // BIT_U16(0) in detect.h -pub(crate) const SIGMATCH_QUOTES_MANDATORY: u16 = 0x40; // BIT_U16(6) in detect.h -pub(crate) const SIGMATCH_INFO_STICKY_BUFFER: u16 = 0x200; // BIT_U16(9) +pub const SIGMATCH_NOOPT: u16 = 1; // BIT_U16(0) in detect.h +pub const SIGMATCH_QUOTES_MANDATORY: u16 = 0x40; // BIT_U16(6) in detect.h +pub const SIGMATCH_INFO_STICKY_BUFFER: u16 = 0x200; // BIT_U16(9) /// cbindgen:ignore extern { diff --git a/scripts/setup-app-layer.py b/scripts/setup-app-layer.py index f94e68ae7d7e..6bbc96d3e69b 100755 --- a/scripts/setup-app-layer.py +++ b/scripts/setup-app-layer.py @@ -210,6 +210,8 @@ def logger_patch_output_c(proto): if line.find("rs_template_logger_log") > -1: output.write(inlines[i].replace("TEMPLATE", proto.upper()).replace( "template", proto.lower())) + # RegisterSimpleJsonApplayerLogger( on itw own line for clang-format + output.write(inlines[i-1]) if line.find("OutputTemplateLogInitSub(") > -1: output.write(inlines[i].replace("Template", proto)) output.write(inlines[i+1]) diff --git a/src/output.c b/src/output.c index 6af423fb4744..2c012e29a116 100644 --- a/src/output.c +++ b/src/output.c @@ -894,7 +894,8 @@ void OutputRegisterRootLoggers(void) RegisterSimpleJsonApplayerLogger(ALPROTO_WEBSOCKET, rs_websocket_logger_log, NULL); RegisterSimpleJsonApplayerLogger(ALPROTO_LDAP, rs_ldap_logger_log, NULL); RegisterSimpleJsonApplayerLogger(ALPROTO_DOH2, AlertJsonDoh2, NULL); - RegisterSimpleJsonApplayerLogger(ALPROTO_TEMPLATE, rs_template_logger_log, NULL); + RegisterSimpleJsonApplayerLogger( + ALPROTO_TEMPLATE, (EveJsonSimpleTxLogFunc)rs_template_logger_log, NULL); RegisterSimpleJsonApplayerLogger(ALPROTO_RDP, (EveJsonSimpleTxLogFunc)rs_rdp_to_json, NULL); // special case : http2 is logged in http object RegisterSimpleJsonApplayerLogger(ALPROTO_HTTP2, rs_http2_log_json, "http");