From 3262dbd72c5d1dad079c7a3ffcb2f117b6b9a5ab Mon Sep 17 00:00:00 2001 From: Szilard Parrag Date: Fri, 6 Sep 2024 13:45:01 +0200 Subject: [PATCH 1/5] lib/filterx: add startswith Signed-off-by: Szilard Parrag --- lib/filterx/CMakeLists.txt | 2 + lib/filterx/Makefile.am | 2 + lib/filterx/func-str.c | 244 +++++++++++++++++++++++++++++++++++++ lib/filterx/func-str.h | 51 ++++++++ 4 files changed, 299 insertions(+) create mode 100644 lib/filterx/func-str.c create mode 100644 lib/filterx/func-str.h diff --git a/lib/filterx/CMakeLists.txt b/lib/filterx/CMakeLists.txt index 3db0f51b14..95f34c96fb 100644 --- a/lib/filterx/CMakeLists.txt +++ b/lib/filterx/CMakeLists.txt @@ -43,6 +43,7 @@ set(FILTERX_HEADERS filterx/func-len.h filterx/func-vars.h filterx/func-unset-empties.h + filterx/func-str.h filterx/func-str-transform.h filterx/func-flatten.h filterx/func-sdata.h @@ -97,6 +98,7 @@ set(FILTERX_SOURCES filterx/func-len.c filterx/func-vars.c filterx/func-unset-empties.c + filterx/func-str.c filterx/func-str-transform.c filterx/func-flatten.c filterx/func-sdata.c diff --git a/lib/filterx/Makefile.am b/lib/filterx/Makefile.am index 7cd6b732e9..8c1db9e69f 100644 --- a/lib/filterx/Makefile.am +++ b/lib/filterx/Makefile.am @@ -46,6 +46,7 @@ filterxinclude_HEADERS = \ lib/filterx/func-vars.h \ lib/filterx/func-unset-empties.h \ lib/filterx/func-str-transform.h \ + lib/filterx/func-str.h \ lib/filterx/func-flatten.h \ lib/filterx/func-sdata.h \ lib/filterx/filterx-private.h \ @@ -100,6 +101,7 @@ filterx_sources = \ lib/filterx/func-vars.c \ lib/filterx/func-unset-empties.c \ lib/filterx/func-str-transform.c \ + lib/filterx/func-str.c \ lib/filterx/func-flatten.c \ lib/filterx/func-sdata.c \ lib/filterx/filterx-private.c \ diff --git a/lib/filterx/func-str.c b/lib/filterx/func-str.c new file mode 100644 index 0000000000..10d332fa35 --- /dev/null +++ b/lib/filterx/func-str.c @@ -0,0 +1,244 @@ +/* + * Copyright (c) 2024 Axoflow + * Copyright (c) 2024 Szilard Parrag + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + * As an additional exemption you are allowed to compile & link against the + * OpenSSL libraries as published by the OpenSSL project. See the file + * COPYING for details. + * + */ + +#include "filterx/func-str.h" +#include "filterx/object-primitive.h" +#include "filterx/filterx-object.h" +#include "filterx/expr-literal.h" +#include "filterx/filterx-eval.h" +#include "object-extractor.h" +#include "object-string.h" +#include "filterx/filterx-object.h" + + +#define FILTERX_FUNC_STARTSWITH_USAGE "Usage: startswith(my_string, my_prefix)" + +static FilterXExpr * +_extract_haystack_arg(FilterXFunctionArgs *args, GError **error, const gchar *function_usage) +{ + gsize len = filterx_function_args_len(args); + if (len != 2) + { + g_set_error(error, FILTERX_FUNCTION_ERROR, FILTERX_FUNCTION_ERROR_CTOR_FAIL, + "invalid number of arguments. %s", function_usage); + return NULL; + } + FilterXExpr *haystack = filterx_function_args_get_expr(args, 0); + if (!haystack) + { + g_set_error(error, FILTERX_FUNCTION_ERROR, FILTERX_FUNCTION_ERROR_CTOR_FAIL, + "haystack must be set. %s", function_usage); + return NULL; + } + return haystack; +} + +static gboolean +_extract_needle_arg(FilterXExprOrLiteral *needle, gboolean ignore_case, FilterXFunctionArgs *args, GError **error, + const gchar *function_usage) +{ + gsize len = filterx_function_args_len(args); + if (len != 2) + { + g_set_error(error, FILTERX_FUNCTION_ERROR, FILTERX_FUNCTION_ERROR_CTOR_FAIL, + "invalid number of arguments. %s", function_usage); + return FALSE; + } + FilterXExpr *needle_expr = filterx_function_args_get_expr(args, 1); + if (!filterx_expr_is_literal(needle_expr)) + { + needle->expr = filterx_expr_ref(needle_expr); + return TRUE; + } + + gsize needle_str_len; + const gchar *needle_str = filterx_function_args_get_literal_string(args, 1, &needle_str_len); + if (!needle_str) + return FALSE; + + if (ignore_case) + { + needle->literal.str = g_utf8_casefold(needle_str, needle_str_len); + needle->literal.str_len = g_utf8_strlen(needle->literal.str, -1); + } + else + { + needle->literal.str = g_strdup(needle_str); + needle->literal.str_len = needle_str_len; + } + return TRUE; +} + +static gboolean +_extract_optional_args(gboolean *ignore_case, FilterXFunctionArgs *args, GError **error, const gchar *function_usage) +{ + gboolean exists, eval_error; + gboolean value = filterx_function_args_get_named_literal_boolean(args, "ignorecase", &exists, &eval_error); + if (!exists) + return TRUE; + + if (eval_error) + { + g_set_error(error, FILTERX_FUNCTION_ERROR, FILTERX_FUNCTION_ERROR_CTOR_FAIL, + "ignorecase argument must be boolean literal. %s", function_usage); + return FALSE; + } + + *ignore_case = value; + return TRUE; +} + +static gboolean +_eval_needle_expr(FilterXExpr *needle_expr, gboolean ignorecase, gchar **needle_str, gsize *needle_len) +{ + FilterXObject *needle_obj = filterx_expr_eval_typed(needle_expr); + if (!needle_obj) + { + filterx_eval_push_error_info("failed to evaluate needle", needle_expr, + g_strdup_printf("invalid expression"), TRUE); + return FALSE; + } + const gchar *needle_str_res; + if (!filterx_object_extract_string(needle_obj, &needle_str_res, needle_len)) + { + filterx_eval_push_error_info("failed to extract needle, it must be a string", needle_expr, + g_strdup_printf("got %s instead", needle_obj->type->name), TRUE); + filterx_object_unref(needle_obj); + return FALSE; + } + if (!ignorecase) + *needle_str = g_strdup(needle_str_res); + else + { + *needle_str = g_utf8_casefold(needle_str_res, *needle_len); + *needle_len = g_utf8_strlen(*needle_str, -1); + } + filterx_object_unref(needle_obj); + return TRUE; +} + +static gboolean +_eval_haystack_expr(FilterXExpr *haystack, gboolean ignorecase, gchar **haystack_str, gsize *haystack_len) +{ + + FilterXObject *haystack_obj = filterx_expr_eval(haystack); + if (!haystack_obj) + { + filterx_eval_push_error_info("failed to evaluate haystack", haystack, + g_strdup_printf("invalid expression"), TRUE); + return FALSE; + } + const gchar *haystack_str_res; + if (!filterx_object_extract_string(haystack_obj, &haystack_str_res, haystack_len)) + { + filterx_eval_push_error_info("failed to extract haystack, it must be a string", haystack, + g_strdup_printf("got %s instead", haystack_obj->type->name), TRUE); + filterx_object_unref(haystack_obj); + return FALSE; + } + if (!ignorecase) + { + *haystack_str = g_strdup(haystack_str_res); + } + else + { + *haystack_str = g_utf8_casefold(haystack_str_res, *haystack_len); + *haystack_len = g_utf8_strlen(*haystack_str, -1); + } + filterx_object_unref(haystack_obj); + return TRUE; +} + + +FilterXObject * +_filterx_function_startswith_eval(FilterXExpr *s) +{ + FilterXFuncStartsWith *self = (FilterXFuncStartsWith *) s; + + gsize haystack_len; + gchar *haystack_str; + if(!_eval_haystack_expr(self->haystack, self->ignore_case, &haystack_str, &haystack_len)) + return NULL; + gsize needle_len; + gchar *needle_str; + if(!self->needle.expr) + { + needle_str = self->needle.literal.str; + needle_len = self->needle.literal.str_len; + } + else if (!_eval_needle_expr(self->needle.expr, self->ignore_case, &needle_str, &needle_len)) + return NULL; + gboolean startswith = FALSE; + if (needle_len > haystack_len) + goto exit; + + if (memcmp(haystack_str, needle_str, needle_len) == 0) + startswith = TRUE; + +exit: + g_free(haystack_str); + if(self->needle.expr) + g_free(needle_str); + return filterx_boolean_new(startswith); +} + + +static void +_filterx_function_startswith_free(FilterXExpr *s) +{ + FilterXFuncStartsWith *self = (FilterXFuncStartsWith *) s; + + filterx_expr_unref(self->haystack); + if (self->needle.expr) + filterx_expr_unref(self->needle.expr); + + filterx_function_free_method(&self->super); +} + +FilterXExpr * +filterx_function_startswith_new(const gchar *function_name, FilterXFunctionArgs *args, GError **error) +{ + FilterXFuncStartsWith *self = g_new0(FilterXFuncStartsWith, 1); + filterx_function_init_instance(&self->super, function_name); + self->ignore_case = FALSE; + if(!_extract_optional_args(&self->ignore_case, args, error, FILTERX_FUNC_STARTSWITH_USAGE)) + goto error; + + self->haystack = _extract_haystack_arg(args, error, FILTERX_FUNC_STARTSWITH_USAGE); + if (!self->haystack) + goto error; + if (!(_extract_needle_arg(&self->needle, self->ignore_case, args, error, FILTERX_FUNC_STARTSWITH_USAGE)) + || !filterx_function_args_check(args, error)) + goto error; + + self->super.super.eval = _filterx_function_startswith_eval; + self->super.super.free_fn = _filterx_function_startswith_free; + filterx_function_args_free(args); + return &self->super.super; + +error: + filterx_function_args_free(args); + filterx_expr_unref(&self->super.super); + return NULL; +} diff --git a/lib/filterx/func-str.h b/lib/filterx/func-str.h new file mode 100644 index 0000000000..be3d6a6c33 --- /dev/null +++ b/lib/filterx/func-str.h @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2024 Axoflow + * Copyright (c) 2024 Szilard Parrag + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + * As an additional exemption you are allowed to compile & link against the + * OpenSSL libraries as published by the OpenSSL project. See the file + * COPYING for details. + * + */ + +#ifndef FILTERX_FUNC_STR_H_INCLUDED +#define FILTERX_FUNC_STR_H_INCLUDED + +#include "filterx/expr-function.h" + +typedef struct _FilterXExprOrLiteral +{ + FilterXExpr *expr; + struct + { + gchar *str; + gssize str_len; + } literal; +} FilterXExprOrLiteral; + +typedef struct _FilterXFuncStartsWith +{ + FilterXFunction super; + FilterXExpr *haystack; + FilterXExprOrLiteral needle; + gsize needle_len; + gboolean ignore_case; +} FilterXFuncStartsWith; + +FilterXExpr *filterx_function_startswith_new(const gchar *function_name, FilterXFunctionArgs *args, GError **error); + +#endif From 306ecea1c2b3b659a4c65947aa683b3f1eef7e42 Mon Sep 17 00:00:00 2001 From: Szilard Parrag Date: Fri, 6 Sep 2024 13:45:17 +0200 Subject: [PATCH 2/5] lib/filterx: add endswith Signed-off-by: Szilard Parrag --- lib/filterx/func-str.c | 74 ++++++++++++++++++++++++++++++++++++++++++ lib/filterx/func-str.h | 10 ++++++ 2 files changed, 84 insertions(+) diff --git a/lib/filterx/func-str.c b/lib/filterx/func-str.c index 10d332fa35..7db9dc8841 100644 --- a/lib/filterx/func-str.c +++ b/lib/filterx/func-str.c @@ -33,6 +33,7 @@ #define FILTERX_FUNC_STARTSWITH_USAGE "Usage: startswith(my_string, my_prefix)" +#define FILTERX_FUNC_ENDSWITH_USAGE "Usage: endswith(my_string, my_prefix)" static FilterXExpr * _extract_haystack_arg(FilterXFunctionArgs *args, GError **error, const gchar *function_usage) @@ -242,3 +243,76 @@ filterx_function_startswith_new(const gchar *function_name, FilterXFunctionArgs filterx_expr_unref(&self->super.super); return NULL; } + + +FilterXObject * +_filterx_function_endswith_eval(FilterXExpr *s) +{ + FilterXFuncEndsWith *self = (FilterXFuncEndsWith *) s; + + gsize haystack_len; + gchar *haystack_str; + if(!_eval_haystack_expr(self->haystack, self->ignore_case, &haystack_str, &haystack_len)) + return NULL; + gsize needle_len; + gchar *needle_str; + if(!self->needle.expr) + { + needle_str = self->needle.literal.str; + needle_len = self->needle.literal.str_len; + } + else if (!_eval_needle_expr(self->needle.expr, self->ignore_case, &needle_str, &needle_len)) + return NULL; + gboolean endswith = FALSE; + if (needle_len > haystack_len) + goto exit; + + if (memcmp(haystack_str + haystack_len - needle_len, needle_str, needle_len) == 0) + endswith = TRUE; + +exit: + g_free(haystack_str); + if(self->needle.expr) + g_free(needle_str); + return filterx_boolean_new(endswith); +} + + +static void +_filterx_function_endswith_free(FilterXExpr *s) +{ + FilterXFuncEndsWith *self = (FilterXFuncEndsWith *) s; + + filterx_expr_unref(self->haystack); + if (self->needle.expr) + filterx_expr_unref(self->needle.expr); + + filterx_function_free_method(&self->super); +} + +FilterXExpr * +filterx_function_endswith_new(const gchar *function_name, FilterXFunctionArgs *args, GError **error) +{ + FilterXFuncEndsWith *self = g_new0(FilterXFuncEndsWith, 1); + filterx_function_init_instance(&self->super, function_name); + self->ignore_case = FALSE; + if(!_extract_optional_args(&self->ignore_case, args, error, FILTERX_FUNC_STARTSWITH_USAGE)) + goto error; + + self->haystack = _extract_haystack_arg(args, error, FILTERX_FUNC_ENDSWITH_USAGE); + if (!self->haystack) + goto error; + if (!(_extract_needle_arg(&self->needle, self->ignore_case, args, error, FILTERX_FUNC_ENDSWITH_USAGE)) + || !filterx_function_args_check(args, error)) + goto error; + + self->super.super.eval = _filterx_function_endswith_eval; + self->super.super.free_fn = _filterx_function_endswith_free; + filterx_function_args_free(args); + return &self->super.super; + +error: + filterx_function_args_free(args); + filterx_expr_unref(&self->super.super); + return NULL; +} diff --git a/lib/filterx/func-str.h b/lib/filterx/func-str.h index be3d6a6c33..02f248216b 100644 --- a/lib/filterx/func-str.h +++ b/lib/filterx/func-str.h @@ -46,6 +46,16 @@ typedef struct _FilterXFuncStartsWith gboolean ignore_case; } FilterXFuncStartsWith; +typedef struct _FilterXFuncEndsWith +{ + FilterXFunction super; + FilterXExpr *haystack; + FilterXExprOrLiteral needle; + gsize needle_len; + gboolean ignore_case; +} FilterXFuncEndsWith; + FilterXExpr *filterx_function_startswith_new(const gchar *function_name, FilterXFunctionArgs *args, GError **error); +FilterXExpr *filterx_function_endswith_new(const gchar *function_name, FilterXFunctionArgs *args, GError **error); #endif From b5cd4c6f8c1b41747feecd5c992ec7ece5574330 Mon Sep 17 00:00:00 2001 From: Szilard Parrag Date: Fri, 6 Sep 2024 12:46:06 +0200 Subject: [PATCH 3/5] lib/filterx: register startswith/endswith Signed-off-by: Szilard Parrag --- lib/filterx/filterx-globals.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/filterx/filterx-globals.c b/lib/filterx/filterx-globals.c index 133198ea38..7b982d98c2 100644 --- a/lib/filterx/filterx-globals.c +++ b/lib/filterx/filterx-globals.c @@ -34,6 +34,7 @@ #include "filterx/func-len.h" #include "filterx/func-vars.h" #include "filterx/func-unset-empties.h" +#include "filterx/func-str.h" #include "filterx/func-str-transform.h" #include "filterx/func-flatten.h" #include "filterx/func-sdata.h" @@ -134,6 +135,8 @@ _ctors_init(void) g_assert(filterx_builtin_function_ctor_register("flatten", filterx_function_flatten_new)); g_assert(filterx_builtin_function_ctor_register("is_sdata_from_enterprise", filterx_function_is_sdata_from_enterprise_new)); + g_assert(filterx_builtin_function_ctor_register("startswith", filterx_function_startswith_new)); + g_assert(filterx_builtin_function_ctor_register("endswith", filterx_function_endswith_new)); } static void From 700d4676e92881c339b20df824b04190df85f76f Mon Sep 17 00:00:00 2001 From: Szilard Parrag Date: Fri, 6 Sep 2024 19:51:29 +0200 Subject: [PATCH 4/5] lib/filterx: add E2E test for startswith Signed-off-by: Szilard Parrag --- .../functional_tests/filterx/test_filterx.py | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/tests/light/functional_tests/filterx/test_filterx.py b/tests/light/functional_tests/filterx/test_filterx.py index 4aff1ed4d0..719ab55ff3 100644 --- a/tests/light/functional_tests/filterx/test_filterx.py +++ b/tests/light/functional_tests/filterx/test_filterx.py @@ -2039,3 +2039,38 @@ def test_list_range_check_out_of_range(config, syslog_ng): syslog_ng.start(config) assert file_false.get_stats()["processed"] == 1 assert file_false.read_log() == "foobar\n" + + +def test_startswith_literal(config, syslog_ng): + (file_true, file_false) = create_config( + config, r""" + if (startswith($MSG, "foo")) + { + $MSG = json(); + $MSG.startswith_foo = true; + }; + """, msg="foo", + ) + syslog_ng.start(config) + + assert file_true.get_stats()["processed"] == 1 + assert "processed" not in file_false.get_stats() + assert file_true.read_log() == '{"startswith_foo":true}\n' + + +def test_startswith_expr_ignorecase(config, syslog_ng): + (file_true, file_false) = create_config( + config, r""" + prefix = "FoO"; + if (startswith($MSG, prefix, ignorecase=true)) + { + $MSG = json(); + $MSG.startswith_foo_ignorecase = true; + }; + """, msg="foo", + ) + syslog_ng.start(config) + + assert file_true.get_stats()["processed"] == 1 + assert "processed" not in file_false.get_stats() + assert file_true.read_log() == '{"startswith_foo_ignorecase":true}\n' From 2b0315bf053b7a392660e1fefbeb261c8eeda25b Mon Sep 17 00:00:00 2001 From: Szilard Parrag Date: Fri, 6 Sep 2024 20:27:52 +0200 Subject: [PATCH 5/5] lib/filterx: add E2E test for endswith Signed-off-by: Szilard Parrag --- .../functional_tests/filterx/test_filterx.py | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/tests/light/functional_tests/filterx/test_filterx.py b/tests/light/functional_tests/filterx/test_filterx.py index 719ab55ff3..e915122538 100644 --- a/tests/light/functional_tests/filterx/test_filterx.py +++ b/tests/light/functional_tests/filterx/test_filterx.py @@ -2074,3 +2074,38 @@ def test_startswith_expr_ignorecase(config, syslog_ng): assert file_true.get_stats()["processed"] == 1 assert "processed" not in file_false.get_stats() assert file_true.read_log() == '{"startswith_foo_ignorecase":true}\n' + + +def test_endswith_literal(config, syslog_ng): + (file_true, file_false) = create_config( + config, r""" + if (endswith($MSG, "bar")) + { + $MSG = json(); + $MSG.endswith_bar = true; + }; + """, msg="foobar", + ) + syslog_ng.start(config) + + assert file_true.get_stats()["processed"] == 1 + assert "processed" not in file_false.get_stats() + assert file_true.read_log() == '{"endswith_bar":true}\n' + + +def test_endswith_expr_ignorecase(config, syslog_ng): + (file_true, file_false) = create_config( + config, r""" + suffix = "bAR"; + if (endswith($MSG, suffix, ignorecase=true)) + { + $MSG = json(); + $MSG.endswith_bar_ignorecase = true; + }; + """, msg="foobar", + ) + syslog_ng.start(config) + + assert file_true.get_stats()["processed"] == 1 + assert "processed" not in file_false.get_stats() + assert file_true.read_log() == '{"endswith_bar_ignorecase":true}\n'