Wazuh Engine 的 CMStore:事件处理内容仓库的命名空间、UUID 缓存与并发模型设计
2026/9/14 8:23:06 网站建设 项目流程

Wazuh Engine 的 CMStore:事件处理内容仓库的命名空间、UUID 缓存与并发模型设计

【免费下载链接】wazuhWazuh - The Open Source Security Platform. Unified XDR and SIEM protection for endpoints and cloud workloads.项目地址: https://gitcode.com/GitHub_Trending/wa/wazuh

CMStore 是 Wazuh Engine 的事件处理内容仓库,负责集中管理解码器(decoders)、过滤器(filters)、输出(outputs)、集成(integrations)、键值库(KVDBs)与策略(policy)这些定义事件处理管线的全部资源。本文基于 CMStore 模块文档 及其源码实现展开,帮助读者理解 CMStore 的三层架构(ICMStoreCMStoreNSCacheNS)、UUID 双向缓存机制、命名空间隔离策略与读写并发模型,掌握后你能够在 Engine 的 builder、router、kvdbstore、cmcrud 等消费方中正确读写内容资产,并定位内容同步与缓存一致性问题。

一、CMStore 在 Engine 中的定位

CMStore 是 Wazuh Engine 的单一事实来源(single source of truth):builder 从它读取内容并编译出事件处理管线,backend 最终执行该管线。除了管线构建,CMStore 还对外提供 CRUD 层,被三类外部使用者依赖:

消费方依赖目标用途
buildercmstore::icmstore读取 policy、integrations、decoders、KVDBs 和 outputs,编译出事件处理管线
routercmstore::icmstore使用ICMStoreNSReader与类型定义进行环境构建和路由配置
kvdbstorecmstore::icmstore从 CMStore 读取 KVDB 定义,填充内存中的只读 KVDB 层
cmcrudcmstore::icmstore将 CMStore 的 CRUD 操作通过管理 API 暴露出去
api/testercmstore::icmstore测试 API 使用 CMStore reader 校验与测试管线配置

从源码结构看,这套职责对应 CMakeLists.txt 中的目标划分:cmstore_icmstore(INTERFACE,公共接口与数据类型)、cmstore_cmstore(STATIC,具体实现,链接 yml、base)、cmstore_mocks(GMock 模拟)、cmstore_utestcmstore_ctest(单元/组件测试)。接口层与实现层分离意味着消费方只需依赖接口头文件即可做 mock 测试,这也是该模块可测试性的基础。

二、总体架构:三层结构与命名空间隔离

CMStore 的架构可以概括为"顶层命名空间管理器 + 每命名空间存储 + 每命名空间缓存"三层:

┌─────────────────────────────────────────────┐ │ ICMStore │ │ (namespace management) │ │ createNamespace / deleteNamespace / getNS │ └────────────────┬────────────────────────────┘ │ ┌──────────────────────┼──────────────────────┐ ▼ ▼ ▼ ┌────────────────┐ ┌────────────────┐ ┌────────────────┐ │ CMStoreNS │ │ CMStoreNS │ │ CMStoreNS │ │ ns: "wazuh" │ │ ns: "custom" │ │ ns: "..." │ │ ┌──────────┐ │ │ ┌──────────┐ │ │ │ │ │ CacheNS │ │ │ │ CacheNS │ │ │ │ │ │ UUID↔Name│ │ │ │ UUID↔Name│ │ │ │ │ └──────────┘ │ │ └──────────┘ │ │ │ │ Filesystem: │ │ Filesystem: │ │ decoders/ │ │ decoders/ │ │ decoders/ │ │ filters/ │ │ filters/ │ │ filters/ │ │ outputs/ │ │ outputs/ │ │ outputs/ │ │ integrations/ │ │ integrations/ │ │ integrations/ │ │ kvdbs/ │ │ kvdbs/ │ │ kvdbs/ │ │ policy.json │ │ policy.json │ │ policy.json │ │ cache_ns.json │ │ cache_ns.json │ │ cache_ns.json │ │ │ └────────────────┘ └────────────────┘ └────────────────┘ Consumers: ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ builder │ │ router │ │ kvdbstore│ │ cmcrud │ └──────────┘ └──────────┘ └──────────┘ └──────────┘

命名空间(Namespace)是磁盘上彼此隔离的内容分区:每个命名空间拥有独立目录,内部按资源类型划分子目录,另加一个 policy 文件和一个 UUID 缓存文件。命名空间 ID 由 types.hpp 中的NamespaceId类型约束:

  • 构造时立即校验合法性(isValidName):非空、且仅允许字母数字与下划线(std::isalnum(c) || c == '_'),非法名称直接抛出std::runtime_error
  • 禁用的命名空间名称有三个:outputsystemdefault(源码中定义为FORBIDDEN_NAMESPACES向量,见 cmstore.cpp 第 18-19 行),在创建、删除、重命名时均会被拒绝;
  • std::hash<NamespaceId>特化提供哈希支持,使命名空间 map 可做到 O(1) 查找。

顶层CMStore内部持有unordered_map<NamespaceId, shared_ptr<ICMstoreNS>>与一个shared_mutex(见 cmstore.hpp),并在注释中明确要求:只应存在一个 CMStore 实例,以避免对命名空间的竞态条件。

三、并发模型:两级 shared_mutex

CMStore 采用两级读写锁,读多写少场景下互不阻塞:

作用域粒度
命名空间 map(CMStoreshared_mutex读:shared / 写:unique
每命名空间的文件 + 缓存(CMStoreNSshared_mutex读:shared / 写:unique

从 cmstore.cpp 的实现可以确认这一模型:getNSgetNSReaderexistsNamespacegetNamespaces均取std::shared_lock;而createNamespacedeleteNamespacerenameNamespace均取std::unique_lock。每命名空间的CMStoreNS同样持有mutable std::shared_mutex m_mutex保护文件与缓存访问(见 storens.hpp 第 46 行),所有资源 CRUD 操作的流程统一为:校验 → 加锁 → 更新缓存 → 写文件 → 落盘缓存

此外,命名空间删除还有一个引用保护机制:deleteNamespace()会检查use_count() > 1(map 本身持有一个引用),若检测到仍有活动的shared_ptr读者/写者则中止删除;renameNamespace()采用相同策略,且重命名时若文件系统 rename 失败会把 map 中的旧条目回滚,保证内存与磁盘状态一致。

四、资源类型与资源分类

资源类型枚举定义在 types.hpp 中,实际源码包含 6 个值(比 README 多出一个UNDEFINED = 0兜底值):

enum class ResourceType : uint8_t { UNDEFINED = 0, DECODER = 1, OUTPUT = 2, FILTER = 3, INTEGRATION = 4, KVDB = 5 };

配套提供resourceTypeFromString()/resourceTypeToString()"decoder""output""filter""integration""kvdb"字符串与枚举之间做 constexpr 转换。各资源类型的语义如下:

  • Decoders(解码器)— 解析原始日志事件,可组成父子层级(parent-child hierarchies);
  • Filters(过滤器)— 应用于事件的预处理(pre-filter)/后处理(post-filter)过滤器;
  • Outputs(输出)— 定义处理后的事件发往何处(indexer、alerts 等);
  • Integrations(集成)— 在一个类别(category)下组织 decoders 和 KVDBs 的 UUID 清单(manifest);
  • KVDBs(键值库)— 用于事件富化的键值查找表,内容以 JSON 对象形式存储。

需要说明的是,getResourceTypeFromAssetName()DECODER、OUTPUT、FILTER视为"资产(asset)",资产名采用<type>/<name>/<version>三段式结构(base::Name类型,parts().size()必须等于 3,首段必须匹配类型前缀,见 detail.hpp 中的checkAssetName())。INTEGRATION 与 KVDB 不属于 asset,走独立的读取接口。

集成类别(Categories)

Integration 必须归属于 8 个预定义类别之一(定义于 categories.hpp):

access-managementapplicationscloud-servicesnetwork-activityothersecuritysystem-activityunclassified

Integration构造时会对 category 做存在性校验(exists()),非法类别直接抛错。

五、UUID 系统:资源的规范化标识

每个资源都有一个UUIDv4标识符。当资源通过 YAML/JSON 内容创建时,CMStore 会走upsertUUID()流程(见 storens.hpp 第 78 行声明):

  1. 将内容解析为 JSON;
  2. 若存在/id字段(pathns::JSON_ID_PATH),校验其是否为合法 UUIDv4,合法则原样使用;
  3. 若不存在,生成新 UUIDv4 并注入回内容;
  4. 返回最终 UUID。

UUID 是贯穿 policy、integrations 与所有交叉引用的规范化标识。Policy构造函数会通过cm::store::detail::findDuplicateOrInvalidUUID()(detail.hpp 第 58-85 行)对 integrations、outputs、filters 三个 UUID 数组统一校验:每个 UUID 必须是合法 UUIDv4,且大小写不敏感地检查重复,违规即抛异常。

六、双向缓存 CacheNS 与 cache_ns.json

每个命名空间维护一个内存中的双向缓存(cachens.hpp),提供两类 O(1) 查找:

  • UUID → (Name, ResourceType)m_uuidToEntryMapunordered_map<string, EntryData>);
  • (Name, ResourceType) → UUIDm_nameTypeToUUIDMap(键为std::tuple<string, ResourceType>,配自定义NameTypeHash)。

关键设计点:

  • 非线程安全CacheNS类注释明确标注 "This class is not thread-safe. External synchronization is required",其并发正确性完全由外层CMStoreNSshared_mutex保证;
  • 序列化:缓存序列化为 JSON 数组,落盘文件为cache_ns.json,每次写操作(flush)都会刷新;createNamespace()时初始化为空数组"[]"(见 cmstore.cpp 第 144-151 行);
  • 加载与重建CMStoreNS构造时调用loadCacheFromDisk();若缓存文件损坏则降级为rebuildCacheFromStorage()——扫描该命名空间下所有资源目录,从每个文件中重新提取 UUID 与名称,重建双向映射;重建也失败才抛异常;
  • 条目操作addEntry()(重复的 UUID 或 (name, type) 抛错)、removeEntryByUUID()removeEntryByNameType()getCollection(type)(按类型导出 (UUID, Name) 元组列表)等。

这套"磁盘为准 + 缓存加速 + 损坏可重建"的设计,使得即使cache_ns.json丢失或损坏,命名空间仍可自恢复,代价只是一次全目录扫描。

七、Policy:事件处理管线的完整定义

Policy(datapolicy.hpp)定义命名空间内完整的事件处理管线,其期望的 JSON 格式(摘自源码注释)如下:

{ "type": "policy", "metadata": { "title": "Development 0.0.1" }, "enabled": true, "root_decoder": "5c1df6b6-1458-4b2e-9001-96f67a8b12c8", "origin_space": "space1", "index_unclassified_events": true, "index_discarded_events": true, "cleanup_decoder_variables": true, "filters": [], "enrichments": ["file", "domain-name", "ip", "url", "geo"], "integrations": [ "42e28392-4f5e-473d-89e8-c9030e6fedc2", "a7fe64a2-0a03-414f-8692-8441bdfe6f69", "5c1df6b6-1458-4b2e-9001-96f67a8b12c8", "f61133f5-90b9-49ed-b1d5-0b88cb04355e", "369c3128-9715-4a30-9ff9-22fcac87688b" ], "outputs": [], "hash": "7ab287...5180", "id": "eb5c2519-feff-4789-8542-9a0453cc8690" }

各字段语义与默认值(结合fromJson()解析逻辑):

字段必选说明 / 默认值
metadata/title策略标题,缺失或为空时取"Untitled Policy"
enabled布尔,缺失则抛错
root_decoder是(可为 null)入口解码器 UUID,null 视为空字符串
integrations集成 UUID 有序数组,缺失则抛错
filters过滤链 UUID 数组(每个 filter 带 pre-filter/post-filter 类型),缺失则抛错
enrichments富化插件数组:file、ip、domain-name、url、geo,缺失则抛错
outputs输出 UUID 数组,可省略
origin_space用于输出解析的空间键,默认"UNDEFINED";仅允许字母数字与下划线(正则^[a-zA-Z0-9_]+$),非法字符抛错
index_unclassified_events是否索引未分类事件,缺失则抛错
index_discarded_events是否索引被丢弃事件,缺失则抛错
cleanup_decoder_variables是否清理解码器临时变量,默认 true(源码中该字段缺失时返回 true)
hash完整性校验哈希,可省略
id策略自身的 UUID

策略文件持久化为命名空间目录下的policy.jsonpathns::POLICY_FILE),通过upsertPolicy()/deletePolicy()维护;getPolicy()在策略不存在或读取失败时抛std::runtime_error

Integration 与 KVDB 数据结构

DataIntegration 的期望 JSON 格式(摘自源码注释):

{ "id": "5c1df6b6-1458-4b2e-9001-96f67a8b12c8", "title": "windows", "enabled": true, "category": "security", "default_parent": "85853f26-5779-469b-86c4-c47ee7d400b4", "decoders": [ "85853f26-5779-469b-86c4-c47ee7d400b4", "4aa06596-5ba9-488c-8354-2475705e1257", "4da71af3-fff5-4b67-90d6-51db9e15bc47", "6f8bd7d2-8516-4b2b-a6f1-cc924513c404" ], "kvdbs": [] }

其中default_parent为可选字段,name取自/metadata/title路径。KVDB(datakvdb.hpp)则包含 uuid、name、content(JSON 对象)与 enabled 字段。三种数据类型的字段汇总:

类型命名空间关键字段
Policycm::store::dataTypetitle、enabled、root_decoder、integrations[]、filters[]、enrichments[]、outputs[]、origin_space、hash、index_unclassified_events、index_discarded_events、cleanup_decoder_variables
Integrationcm::store::dataTypeuuid、name、enabled、category、default_parent?、decoders[]、kvdbs[]
KVDBcm::store::dataTypeuuid、name、content(JSON 对象)、enabled

八、资产适配:adaptDecoder / adaptFilter

detail.hpp 还提供adaptDecoder()adaptFilter()两个函数,在资源交给 builder 消费之前,将 YAML/JSON 文档归一化为规范的键顺序。以解码器为例,规范化顺序为:

name → parents → definitions → check → parse|* → normalize → enabled → id

源码中adaptDecoder()逐字段处理:先提取并校验/name(必须是decoder/...三段式合法名称),再依次透传parents等字段。适配层保证了无论上游文档字段顺序如何,builder 看到的都是同一规范结构——这是"内容仓库与管线编译器解耦"的关键一环。

九、公共接口:ICMStore / ICMStoreNSReader / ICMstoreNS

三个接口全部定义在 icmstore.hpp,按"只读 → 读写"两层切分,消费方可按最小权限原则选择:

9.1 ICMStore(顶层,命名空间管理)

namespace cm::store { class ICMStore { virtual std::shared_ptr<ICMStoreNSReader> getNSReader(const NamespaceId& nsId) const = 0; virtual std::shared_ptr<ICMstoreNS> getNS(const NamespaceId& nsId) = 0; virtual std::shared_ptr<ICMstoreNS> createNamespace(const NamespaceId& nsId) = 0; virtual void deleteNamespace(const NamespaceId& nsId) = 0; virtual void renameNamespace(const NamespaceId& from, const NamespaceId& to) = 0; virtual bool existsNamespace(const NamespaceId& nsId) const = 0; virtual std::vector<NamespaceId> getNamespaces() const = 0; }; }

9.2 ICMStoreNSReader(命名空间只读视图)

builder 与 router 通过它读取管线定义。核心方法分五组:

class ICMStoreNSReader { // 通用 virtual const NamespaceId& getNamespaceId() const = 0; virtual std::vector<std::tuple<std::string, std::string>> getCollection(ResourceType type) const = 0; virtual std::tuple<std::string, ResourceType> resolveNameFromUUID(const std::string& uuid) const = 0; virtual std::string resolveUUIDFromName(const std::string& name, ResourceType type) const = 0; virtual bool assetExistsByName(const base::Name& name) const = 0; virtual bool assetExistsByUUID(const std::string& uuid) const = 0; // Policy virtual dataType::Policy getPolicy() const = 0; // Integrations virtual dataType::Integration getIntegrationByName(const std::string& name) const = 0; virtual dataType::Integration getIntegrationByUUID(const std::string& uuid) const = 0; // KVDBs virtual dataType::KVDB getKVDBByName(const std::string& name) const = 0; virtual dataType::KVDB getKVDBByUUID(const std::string& uuid) const = 0; // Assets (decoders, filters, outputs) virtual json::Json getAssetByName(const base::Name& name) const = 0; virtual json::Json getAssetByUUID(const std::string& uuid) const = 0; virtual const std::vector<json::Json> getOutputsForSpace(std::string_view spaceKey) const = 0; // 模板辅助 template<typename T> auto getResourceByName(const std::string& name) const; template<typename T> auto getResourceByUUID(const std::string& uuid) const; };

两个模板辅助函数用if constexpr在编译期把返回类型分派到Integration/KVDB/json::Json(asset),对不支持的 T 触发static_assert,让调用方一行代码即可按类型取资源。getOutputsForSpace()的解析规则在头文件注释中写明:若 outputs 路径下存在以给定 space key 命名的目录,则从该目录加载 outputs;否则回退到default/目录。

9.3 ICMstoreNS(读写扩展)

class ICMstoreNS : public ICMStoreNSReader { virtual std::string createResource(const std::string& name, ResourceType type, const json::Json& content) = 0; virtual void updateResourceByName(const std::string& name, ResourceType type, const json::Json& content) = 0; virtual void updateResourceByUUID(const std::string& uuid, const json::Json& content) = 0; virtual void deleteResourceByName(const std::string& name, ResourceType type) = 0; virtual void deleteResourceByUUID(const std::string& uuid) = 0; virtual void upsertPolicy(const dataType::Policy& policy) = 0; virtual void deletePolicy() = 0; };

需要注意一个实现细节:README 文档中写的是const std::string& ymlContent,而当前源码签名实际为const json::Json& content——即 CRUD 层接收的是已解析的 JSON 对象,而非原始 YAML 字符串;YAML→JSON 的解析发生在调用方(如 cmcrud 的 API 层)。接口语法还特别提醒:createResource/updateResource*不校验内容是否匹配资源类型的 schema,schema 校验由上层负责。

十、实现细节:磁盘布局、路径映射与权限

具体实现分布在 src/cmstore.cpp、src/storens.cpp、src/cachens.cpp 与文件工具 src/fileutils.hpp。要点如下:

CMStore 构造函数CMStore(path, outputsPath)):

  1. 两个路径都必须绝对路径已存在的目录,否则抛std::runtime_error
  2. 写权限探测:在 basePath 下尝试创建临时文件.wazuh_test_write_permission与临时目录.wazuh_test_dir_permission,成功则删除并继续,失败则带errno/error_code信息抛错(源码刻意"avoiding check mode_t",用实际写测试代替权限位检查);
  3. 调用loadAllNamespacesFromDisk():遍历 basePath 下所有子目录,以目录名构造NamespaceId(自动触发命名合法性校验),跳过禁用命名空间,为每个命名空间构造CMStoreNS实例装入 map。

CMStoreNS 磁盘布局与 CRUD

  • 命名空间目录下按资源类型划分子目录,常量集中定义在 storens.hpp 第 17-32 行的pathns命名空间:decoders/filters/outputs/integrations/kvdbs/,以及文件常量policy.json(策略)、cache_ns.json(缓存)、.json(资产扩展名);
  • 路径安全:资源名中的/在落盘文件名中被替换为_,资源类型决定所在子目录(getResourcePaths());
  • 存储格式:资产统一存为.json,策略存为policy.json
  • 文件权限:文件 0640、目录 0750(由fileutils::setDirectoryPermissions()等保证);
  • 输出解析getOutputsForSpace()优先使用 space key 专属目录,缺失时回退default/(对应pathns::DEFAULT_OUTPUTS_DIR)。

缓存一致性:任何写操作(create/update/delete resource、upsertPolicy 等)都遵循"更新内存缓存 → 写资源文件 → flush 缓存到磁盘"的顺序,配合两级 shared_mutex,保证读侧看到的缓存与磁盘最终一致。

十一、目录结构与构建目标

模块的完整目录布局(以当前仓库为准):

src/engine/source/cmstore/ ├── CMakeLists.txt ├── interface/cmstore/ # 公共接口 │ ├── icmstore.hpp # ICMStore, ICMstoreNS, ICMStoreNSReader │ ├── types.hpp # ResourceType 枚举, NamespaceId, 数据类型导入 │ ├── categories.hpp # AVAILABLE_CATEGORIES, exists() │ ├── detail.hpp # adaptDecoder(), adaptFilter(), UUID 校验 │ ├── datapolicy.hpp # dataType::Policy — 管线定义 │ ├── dataintegration.hpp # dataType::Integration — 集成清单 │ └── datakvdb.hpp # dataType::KVDB — 键值库定义 ├── include/cmstore/ │ └── cmstore.hpp # CMStore — 顶层具体实现 ├── src/ │ ├── cmstore.cpp # CMStore:命名空间生命周期、磁盘加载 │ ├── storens.hpp # CMStoreNS — 每命名空间实现 │ ├── storens.cpp # CRUD、policy、integration、KVDB、assets │ ├── cachens.hpp # CacheNS — UUID↔Name 双向缓存 │ ├── cachens.cpp # 缓存序列化、增删查 │ └── fileutils.hpp # 文件 I/O 辅助(upsert、read、delete、权限) ├── test/ │ ├── mocks/cmstore/ │ │ └── mockcmstore.hpp # GMock: MockICMStoreNSReader, MockICMstoreNS, MockICMstore │ └── src/ │ ├── unit/ │ │ ├── cachens_test.cpp # CacheNS 单元测试 │ │ ├── cmstore_test.cpp # CMStore/CMStoreNS 单元测试 │ │ └── detail_test.cpp # detail 辅助函数测试 │ └── component/ │ └── cmstore_test.cpp # 基于真实文件系统的组件测试 └── benchmark/src/ └── cmsync_bench.cpp # 同步基准测试

CMake 目标一览(与 CMakeLists.txt 一致):

目标类型别名说明
cmstore_icmstoreINTERFACEcmstore::icmstore公共接口 + 数据类型(链接 base)
cmstore_cmstoreSTATICcmstore::cmstore具体实现(链接 yml、base)
cmstore_mocksINTERFACEcmstore::mocks所有接口的 GMock 模拟(链接 GTest::gmock)
cmstore_utestExecutable单元测试(cache、store、detail)
cmstore_ctestExecutable组件测试(真实文件系统)

十二、测试策略

模块的测试分三层:

  1. 单元测试(test/src/unit/):cachens_test.cpp覆盖 CacheNS 的双向查找、序列化/反序列化、冲突检测;cmstore_test.cpp覆盖 CMStore 命名空间操作与 CMStoreNS 行为;detail_test.cpp覆盖adaptDecoder/adaptFilter/UUID 校验等辅助函数;
  2. 组件测试(test/src/component/cmstore_test.cpp):使用真实文件系统执行完整的资源 CRUD 周期,验证"文件 + 缓存 + 策略"三者的端到端一致性;
  3. Mock 层(test/mocks/cmstore/mockcmstore.hpp):为ICMStoreICMstoreNSICMStoreNSReader三个接口提供 GMock 派生类,供 builder、cmcrud 等下游模块的测试注入。

此外benchmark/src/cmsync_bench.cpp提供内容同步路径的基准测试,用于度量命名空间同步在大内容量下的开销。

小结

CMStore 用"顶层 map + 每命名空间目录 + 每命名空间双向缓存"的最小化设计,支撑了 Wazuh Engine 内容管理的全部需求:命名空间提供内容隔离与独立生命周期;UUIDv4 双向缓存让跨资源交叉引用(policy ↔ integrations ↔ decoders/KVDBs)保持 O(1) 解析;两级shared_mutex让 builder/router 的高频读与 cmcrud 的低频写互不阻塞;而cache_ns.json的可重建性则为磁盘损坏场景提供了自恢复能力。对需要理解 Engine 管线如何从"内容"编译为"执行"的读者,CMStore 是必须先看的一块基石;继续深入时,建议从 icmstore.hpp 的三个接口读起,再对照 cmstore.cpp 与 storens.cpp 的实现,最后用 组件测试 验证你对 CRUD 周期的理解。

【免费下载链接】wazuhWazuh - The Open Source Security Platform. Unified XDR and SIEM protection for endpoints and cloud workloads.项目地址: https://gitcode.com/GitHub_Trending/wa/wazuh

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询