From 84a90873e9f660a5c795c6151e20dbe4c4738f28 Mon Sep 17 00:00:00 2001 From: Samaresh Kumar Singh Date: Sun, 26 Jul 2026 11:54:58 -0500 Subject: [PATCH] conf-yaml: support glob patterns in include directive SCConfYamlHandleInclude only accepted explicit filenames. The rule-files directive has supported shell-style glob expansion via glob(3) for years, and asymmetry with include has been blocking clean drop-in conf.d/ style configuration directories. Refactor SCConfYamlHandleInclude into a thin wrapper that resolves the path against conf_dirname and, if the input contains glob metacharacters (*, ?, [), expands it with glob(3) and includes each match in lexicographic order. A pattern that matches no files is logged as a warning and not treated as an error, so a drop-in directory can be empty without breaking startup. Literal paths skip glob() and call the existing per-file inclusion logic directly, preserving current behaviour. Glob expansion needs glob(3), which is not available everywhere. Windows builds have no glob.h, so an include pattern used to fall through and be opened as a literal filename, failing with a confusing errno. On such platforms the pattern is now logged as a warning and skipped instead. Literal includes are unaffected. The behaviour is verified by the config-includes-glob-order suricata-verify test, which checks that every file matching a pattern is loaded and that matches are included in deterministic lexicographic order. Document the new behaviour, including the platform limitation, in doc/userguide/configuration/includes.rst. Feature: #8427. --- doc/userguide/configuration/includes.rst | 23 ++++++ src/conf-yaml-loader.c | 92 ++++++++++++++++++++---- 2 files changed, 100 insertions(+), 15 deletions(-) diff --git a/doc/userguide/configuration/includes.rst b/doc/userguide/configuration/includes.rst index 2fc62908aa..9a758a3f2e 100644 --- a/doc/userguide/configuration/includes.rst +++ b/doc/userguide/configuration/includes.rst @@ -49,6 +49,29 @@ is the equivalent of:: address-groups: HOME_NET: "[192.168.0.0/16,10.0.0.0/8,172.16.0.0/12]" +Glob Patterns +------------- + +Filenames in ``include`` may contain shell-style glob metacharacters +(``*``, ``?``, ``[...]``). Patterns are expanded at startup via +``glob(3)`` and each matching file is loaded in lexicographic order. A +pattern that matches no files is logged as a warning and is not treated +as an error, which allows drop-in ``conf.d/``-style directories to be +empty. + +:: + + include: + - /etc/suricata/conf.d/*.yaml + +Relative patterns are resolved against the directory of the top-level +configuration file, the same as literal includes. + +.. note:: Glob expansion requires ``glob(3)``, which is not available on + all platforms. On platforms without it, notably Windows, an + ``include`` containing glob metacharacters is skipped with a warning + and no files are loaded from it. Literal filenames are unaffected. + .. note:: Suricata versions less than 7 required multiple ``include`` statements to be specified to include more than one file. While Suricata 7.0 still supports this it will issue a deprecation diff --git a/src/conf-yaml-loader.c b/src/conf-yaml-loader.c index a011ba697d..013e76dbe8 100644 --- a/src/conf-yaml-loader.c +++ b/src/conf-yaml-loader.c @@ -27,6 +27,9 @@ #include "conf.h" #include "conf-yaml-loader.h" #include +#ifdef HAVE_GLOB_H +#include +#endif #include "util-path.h" #include "util-debug.h" #include "util-unittest.h" @@ -104,18 +107,17 @@ ConfYamlSetConfDirname(const char *filename) } /** - * \brief Include a file in the configuration. + * \brief Include a single resolved file in the configuration. * * \param parent The configuration node the included configuration will be * placed at. - * \param filename The filename to include. + * \param filename The fully resolved filename to include. * * \retval 0 on success, -1 on failure. */ -int SCConfYamlHandleInclude(SCConfNode *parent, const char *filename) +static int ConfYamlHandleIncludeOne(SCConfNode *parent, const char *filename) { yaml_parser_t parser; - char include_filename[PATH_MAX]; FILE *file = NULL; int ret = -1; @@ -124,18 +126,9 @@ int SCConfYamlHandleInclude(SCConfNode *parent, const char *filename) return -1; } - if (PathIsAbsolute(filename)) { - strlcpy(include_filename, filename, sizeof(include_filename)); - } - else { - snprintf(include_filename, sizeof(include_filename), "%s/%s", - conf_dirname, filename); - } - - file = fopen(include_filename, "r"); + file = fopen(filename, "r"); if (file == NULL) { - SCLogError("Failed to open configuration include file %s: %s", include_filename, - strerror(errno)); + SCLogError("Failed to open configuration include file %s: %s", filename, strerror(errno)); goto done; } @@ -157,6 +150,75 @@ done: return ret; } +/** + * \brief Include a file or glob pattern in the configuration. + * + * Relative paths are resolved against the directory of the top-level config + * file. If the input contains glob metacharacters (\c *, \c ?, \c [) the + * pattern is expanded via glob(3) and each match is included in lexicographic + * order. A pattern that matches no files is logged as a warning and not + * treated as an error, to support drop-in `conf.d/` directories. + * + * Glob expansion needs glob(3). On platforms without it, notably Windows, a + * pattern is logged as a warning and skipped instead of being opened as a + * literal filename. + * + * \param parent The configuration node the included configuration will be + * placed at. + * \param filename The filename or glob pattern to include. + * + * \retval 0 on success, -1 on failure. + */ +int SCConfYamlHandleInclude(SCConfNode *parent, const char *filename) +{ + char include_filename[PATH_MAX]; + + if (PathIsAbsolute(filename)) { + strlcpy(include_filename, filename, sizeof(include_filename)); + } else { + snprintf(include_filename, sizeof(include_filename), "%s/%s", conf_dirname, filename); + } + + const bool is_pattern = strpbrk(filename, "*?[") != NULL; + +#ifdef HAVE_GLOB_H + if (is_pattern) { + glob_t globbuf; + int gret = glob(include_filename, 0, NULL, &globbuf); + + if (gret == GLOB_NOMATCH) { + SCLogWarning("No files match include pattern %s", include_filename); + return 0; + } else if (gret != 0) { + SCLogError( + "Failed to expand include pattern %s: %s", include_filename, strerror(errno)); + return -1; + } + + int ret = 0; + for (size_t i = 0; i < (size_t)globbuf.gl_pathc; i++) { + const char *path = globbuf.gl_pathv[i]; + SCLogInfo("Including configuration file %s (matched %s).", path, filename); + if (ConfYamlHandleIncludeOne(parent, path) != 0) { + ret = -1; + break; + } + } + globfree(&globbuf); + return ret; + } +#else + if (is_pattern) { + SCLogWarning("Glob patterns in include are not supported on this platform, " + "skipping include %s", + include_filename); + return 0; + } +#endif + + return ConfYamlHandleIncludeOne(parent, include_filename); +} + /** * \brief Parse a YAML layer. *