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 6e455c5453..04f9116bae 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" @@ -103,18 +106,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; @@ -123,18 +125,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; } @@ -156,6 +149,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. *