Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Applayer plugin example and doc #12441

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/workflows/builds.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion configure.ac
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
49 changes: 47 additions & 2 deletions doc/userguide/devguide/libsuricata/index.rst
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
.. _libsuricata:

LibSuricata
===========
LibSuricata and Plugins
=======================

Using Suricata as a Library
---------------------------
Expand All @@ -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``
5 changes: 5 additions & 0 deletions examples/plugins/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
16 changes: 16 additions & 0 deletions examples/plugins/altemplate/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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 = []
2 changes: 2 additions & 0 deletions examples/plugins/altemplate/altemplate.rules
Original file line number Diff line number Diff line change
@@ -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;)
17 changes: 17 additions & 0 deletions examples/plugins/altemplate/altemplate.yaml
Original file line number Diff line number Diff line change
@@ -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
103 changes: 103 additions & 0 deletions examples/plugins/altemplate/src/detect.rs
Original file line number Diff line number Diff line change
@@ -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,
);
}
6 changes: 6 additions & 0 deletions examples/plugins/altemplate/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
mod detect;
mod log;
mod parser;
pub mod plugin;
mod suricata;
mod template;
46 changes: 46 additions & 0 deletions examples/plugins/altemplate/src/log.rs
Original file line number Diff line number Diff line change
@@ -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()
}
66 changes: 66 additions & 0 deletions examples/plugins/altemplate/src/parser.rs
Original file line number Diff line number Diff line change
@@ -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<u32, std::num::ParseIntError> {
input.parse::<u32>()
}

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);
}
}
}
}
37 changes: 37 additions & 0 deletions examples/plugins/altemplate/src/plugin.rs
Original file line number Diff line number Diff line change
@@ -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))
}
Loading
Loading