Wazuh Engine 的 cmcrud 模块解析:内容管理器的 CRUD 服务层与"先验证后写入"设计
【免费下载链接】wazuhWazuh - The Open Source Security Platform. Unified XDR and SIEM protection for endpoints and cloud workloads.项目地址: https://gitcode.com/GitHub_Trending/wa/wazuh
cmcrud 是 Wazuh 引擎(engine)中 Content Manager 的 CRUD 服务层,位于 HTTP API 处理器与持久层cmstore之间,负责协调对命名空间(namespace)、策略(policy)与各类资源(decoder、filter、output、integration、KVDB)的全部增删改查操作。本文基于仓库中src/engine/source/cmcrud/README.md及其对应源码实现,完整讲解该模块的架构分层、"验证先于变更"(Validation-Before-Mutation)原则、资源导入的依赖顺序与回滚机制、弱指针生命周期模型,并给出构建目标与测试组织方式,帮助读者理解 Wazuh 引擎如何保证每一份持久化到cmstore的内容工件(artifact)都已通过一致性检查。
架构分层:ICrudService 处于 HTTP 层与持久层之间
从模块 README 给出的架构图可以看到,cmcrud 在调用链中的位置非常清晰:
┌────────────────┐ ┌────────────────┐ │ api/cmcrud │ │ cmsync │ │ (HTTP layer) │ │ (sync service) │ └───────┬────────┘ └───────┬─────────┘ │ ICrudService │ ICrudService ▼ ▼ ┌──────────────────────────────────────────┐ │ CrudService │ │ │ │ • Structured JSON handling │ │ • Asset adaptation (canonical ordering) │ │ • Validation delegation │ │ • Import orchestration with rollback │ └──────┬─────────────────────┬─────────────┘ │ ICMStore │ IValidator ▼ ▼ ┌──────────────┐ ┌─────────────────┐ │ cmstore │ │ builder module │ │ (persistent │ │ (structural │ │ content) │ │ validation) │ └──────────────┘ └─────────────────┘上游有两个消费者,均通过ICrudService接口与 cmcrud 交互,而不直接依赖具体实现:
api/cmcrud:HTTP API 处理器,向外部客户端暴露命名空间与资源的 CRUD 操作。在 handlers.cpp 中可以确认,每个路由处理器(如namespaceList、namespaceCreate、namespaceDelete)都持有std::weak_ptr<cm::crud::ICrudService>,并通过adapter::getReqAndHandler模板将请求分发到 CRUD 服务——这种弱指针持有方式与 cmcrud 内部的生命周期设计相呼应。cmsync:内容同步服务,使用ICrudService从集群导入内容,其核心实现 cmsync.cpp 调用的正是importNamespace的重载之一。
下游则依赖两个模块:持久内容层cmstore(通过ICMStore接口)和builder模块的结构化验证器(通过IValidator接口,其实现在 builder.cpp)。
关键设计约束在 README 中明确写出:在任何变更到达 store 之前,cmcrud 会接收结构化的json::Json载荷,执行类型特定的适配(资产的规范字段排序),并把结构化验证委托给builder::IValidator。这保证了每一份持久化到cmstore的工件都已经被检查过一致性。
验证先于变更:按资源类型分派的验证路径
CrudService从不绕过验证直接写cmstore。根据资源类型,它走不同的验证路径:
| 资源类型 | 验证路径 |
|---|---|
| Policy(策略) | IValidator::softPolicyValidate() |
| Integration(集成) | IValidator::softIntegrationValidate() |
| Asset(decoder / filter / output) | IValidator::validateAsset() |
| KVDB | 通过cm::store::dataType::KVDB::fromJson()解析(仅做结构检查) |
导入路径上的force/softValidation标志可以放松或跳过部分检查。这一点在 cmcrudservice.cpp 中可以直接印证。三个私有验证方法分别封装了对应的委托调用(L758-L779):
void CrudService::validatePolicy(const std::shared_ptr<cm::store::ICMStoreNSReader>& nsReader, const cm::store::dataType::Policy& policy) const { throwIfError(getValidator()->softPolicyValidate(nsReader, policy), fmt::format("Policy validation failed in namespace '{}'", nsReader->getNamespaceId().toStr())); } void CrudService::validateIntegration(...) const { throwIfError(getValidator()->softIntegrationValidate(nsReader, integration), ...); } void CrudService::validateAsset(const std::shared_ptr<cm::store::ICMStoreNSReader>& nsReader, const json::Json& asset) const { throwIfError(getValidator()->validateAsset(nsReader, asset), ...); }注意它们都以nsReader(命名空间只读视图)作为第一个参数传入验证器——这意味着验证器能够基于"命名空间中当前已有什么"来做上下文相关的检查(例如 policy 引用的资源是否存在),而不是孤立地检查单个 JSON 文档。
而 KVDB 的"验证"实际上就是解析:kvdbFromDocument()直接委托给KVDB::fromJson()(L51-L54),解析成功即意味着结构合法,没有额外的验证器调用。
force/softValidation标志的语义在导入流程中体现为条件判断,例如 JSON 文档导入路径中的 integration 分支(L262-L265):
if (!force) { validateIntegration(nsReader, integ); }即force == true时跳过该项验证,直接落库。
Asset 适配:规范字段排序保证确定性序列化
资产(decoder / filter / output)在存储前会经过一个类型特定的adapter,强制执行规范字段排序:
detail::adaptDecoder()— decoder 的规范字段顺序detail::adaptFilter()— filter 的规范字段顺序detail::adaptOutput()— output 的规范字段顺序
这确保了无论用户以何种顺序提供字段,序列化结果都是确定性的。在 cmcrudservice.cpp 中,这三个适配函数在importNamespace的资产分支(L291-L300)、upsertResource的资产分支(L624-L633)以及validateResource中都被以相同的模式调用:
auto assetJson = [&item, type]() -> json::Json { switch (type) { case cm::store::ResourceType::DECODER: return cm::store::detail::adaptDecoder(item); case cm::store::ResourceType::FILTER: return cm::store::detail::adaptFilter(item); case cm::store::ResourceType::OUTPUT: return cm::store::detail::adaptOutput(item); } __builtin_unreachable(); }();适配之后,cmcrud 还会做一项"名字前缀"检查:资产的逻辑名必须形如decoder/...、filter/...、output/...,前缀部分必须与声明的资源类型一致,否则抛出Asset name '{}' does not match resource type '{}'错误(L308-L312)。这从命名层面防止了"把一个 filter 伪装成 decoder 存进去"这类错误。
命名空间导入:严格依赖顺序与 all-or-nothing 回滚
导入(import)是一个"全有或全无"的操作。资源按严格的依赖链顺序摄取:
- KVDB
- Decoder
- Filter(JSON 文档重载下从 integration 中提取)
- Output(JSON 文档重载下从 integration 中提取)
- Integration
- Policy
如果任何一步失败,bestEffortDelete回滚 lambda 会删除该命名空间中已经创建的一切。JSON 文档重载的完整实现见 importNamespace,其执行流程与 README 的描述一一对应,并补充了若干源码级的细节:
- 拒绝覆盖:若目标命名空间已存在,直接抛错——"Import is only allowed into a new namespace"(L170-L175)。
- 严格的结构校验:导入 JSON 必须包含且仅包含
policy与resources两个顶层键,且二者都必须是对象(L191-L225)。文档注释中给出的预期结构为:
{ "policy": { ... }, "resources": { "kvdbs": [ ... ], "decoders": [ ... ], "integrations": [ ... ], "policy": { ... } } }- 注册回滚:
bestEffortDelete是一个捕获 store 的 lambda,调用store->deleteNamespace(id);若回滚删除本身也失败,只记录LOG_WARNING_L警告而不掩盖原始错误(L148-L163)。 - 按序导入:
importResourceslambda 按KVDB → DECODER → FILTER → OUTPUT → INTEGRATION的顺序依次读取/resources/kvdbs、/resources/decoders等数组并逐条入库(L329-L334),随后处理/policy并通过ns->upsertPolicy(policy)落库。 - origin space 记录:若调用方提供了
originSpace(非空),导入的策略会通过policy.setOriginSpace(originSpace)标记来源空间(L344-L348)。 - 失败即回滚:外层
catch块在destinationCreated为真时执行bestEffortDelete(nsId),并抛出一条带命名空间标识与原始错误信息的std::runtime_error(L353-L360)。
importNamespace存在两个重载,职责与适用场景不同:
| 重载 | 输入 | 使用场景 |
|---|---|---|
importNamespace(nsId, jsonDocument, origin, force) | 包含全部组件的单个 JSON 文档 | API 驱动的导入(例如上传完整的命名空间导出) |
importNamespace(nsId, kvdbs, decoders, filters, integrations, policy, softValidation) | 预解析的组件向量 | cmsync的程序化导入 |
从 icmcrudservice.hpp 的接口签名可以看到第二个重载实际接收六个参数向量:kvdbs、decoders、filters、integrations四个std::vector<json::Json>加上policy文档与softValidation布尔标志。其实现(L365-L462)同样先拒绝已存在的命名空间,然后依次循环处理四类资源,最后upsertPolicy。与 JSON 文档重载的一个差异是:组件重载中每个资产/集成使用if (!softValidation) validator->validateAsset/softIntegrationValidate(...)控制验证粒度,即softValidation == true时只保留最关键的检查。
弱指针资源模型:显式的生命周期管理
CrudService把它的两个依赖(ICMStore、IValidator)保存为std::weak_ptr。构造时要求两个shared_ptr均非空,否则抛std::invalid_argument(L72-L85);此后每个公开方法入口都通过getStore()/getValidator()尝试lock(),若底层对象已被销毁则抛出带明确语义的std::runtime_error("CMStore is no longer available" / "Validator is no longer available",L87-L105)。
这种设计带来两个好处:其一,避免CrudService通过强引用意外延长 store/validator 的生命周期,防止悬垂引用;其二,把生命周期问题转化为运行期可诊断的异常,使"底层对象何时可以安全析构"这一行为是显式的。上游api/cmcrud的 handlers 同样以weak_ptr持有ICrudService,说明"弱引用 + 入口锁定"是整条调用链统一的资源持有风格。
公开接口:ICrudService 与 ResourceSummary
接口定义在 icmcrudservice.hpp,命名空间为cm::crud。接口按三组组织,文档头注释明确了实现者的四项职责:使用cm::store::ICMStore解析命名空间;把结构化 JSON 载荷转换为对应数据类型(Policy、Integration、KVDB、资产);在变更底层 store 之前把结构化检查委托给builder::IValidator;成功后应用cm::store变更。错误处理约定为抛出携带描述性消息的std::runtime_error(或派生类型)。
命名空间操作
virtual std::vector<cm::store::NamespaceId> listNamespaces() const = 0; virtual void createNamespace(const cm::store::NamespaceId& nsId) = 0; virtual bool existsNamespace(const cm::store::NamespaceId& nsId) const = 0; virtual void deleteNamespace(const cm::store::NamespaceId& nsId) = 0;createNamespace:若命名空间已存在则实现应失败(接口注释如此约定,实现中会包装为带命名空间名的std::runtime_error,见 L117-L127)。deleteNamespace:删除命名空间及其全部资源。
Policy 操作
virtual void upsertPolicy(const cm::store::NamespaceId& nsId, const json::Json& policy) = 0; virtual void deletePolicy(const cm::store::NamespaceId& nsId) = 0;upsertPolicy的语义(接口注释 + 实现 L464-L481):若命名空间没有 policy 则新建,否则替换;载荷先转换为cm::store::dataType::Policy,验证通过后才落库——这也是"验证先于变更"原则在非导入路径上的体现。
通用资源操作
virtual std::vector<ResourceSummary> listResources(const cm::store::NamespaceId& nsId, cm::store::ResourceType type) const = 0; virtual json::Json getResourceByUUID(const cm::store::NamespaceId& nsId, const std::string& uuid) const = 0; virtual void upsertResource(const cm::store::NamespaceId& nsId, cm::store::ResourceType type, const json::Json& resource) = 0; virtual void deleteResourceByUUID(const cm::store::NamespaceId& nsId, const std::string& uuid) = 0; virtual void validateResource(cm::store::ResourceType type, const json::Json& payload) = 0;配套的轻量目录结构体:
struct ResourceSummary { std::string uuid; ///< Resource UUID (unique within namespace). std::string name; ///< Logical name, e.g. "decoder/apache_access". };validateResource是一个"隔离验证"入口——不绑定任何命名空间,仅按类型校验载荷结构。接口注释中特别注明:对 DECODER 的验证,缺失的 KVDB 引用不应被视为错误(icmcrudservice.hpp#L239),这解释了为什么它对 DECODER/FILTER 走validateAssetShallow()(浅验证)而非完整validateAsset()。
CrudService 具体实现头
cmcrudservice.hpp 中的CrudService final除了实现全部接口方法外,还声明了私有成员与辅助方法:
std::weak_ptr<cm::store::ICMStore> m_store; std::weak_ptr<builder::IValidator> m_validator; std::shared_ptr<cm::store::ICMStore> getStore() const; std::shared_ptr<builder::IValidator> getValidator() const; void validatePolicy(...) const; void validateIntegration(...) const; void validateAsset(...) const; std::shared_ptr<cm::store::ICMStoreNSReader> getNamespaceStoreView(const cm::store::NamespaceId&) const; std::shared_ptr<cm::store::ICMstoreNS> getNamespaceStore(const cm::store::NamespaceId&) const;其中两个命名空间辅助方法分别获取只读视图(getNSReader,用于listResources/getResourceByUUID等读路径)与可写句柄(getNS,用于写路径),二者在命名空间不存在时都抛出Namespace '{}' does not exist(L781-L800)。
匿名命名空间中的辅助函数
cmcrudservice.cpp 顶部的匿名 namespace 定义了一组小型辅助函数:
| 辅助函数 | 作用 |
|---|---|
assetUuidFromJson(json, assetName) | 从资产 JSON 文档提取/id字段,缺失或为空则抛错;还通过base::utils::generators::isValidUUIDv4()校验必须是合法 UUIDv4(L14-L30) |
assetNameFromJson(json) | 从资产 JSON 提取/name逻辑名,缺失或为空则抛错 |
throwIfError(base::OptError, context) | 把OptError转换为抛出的std::runtime_error,错误消息带上上下文前缀 |
policyFromDocument(const json::Json&) | 把 JSON 对象转换为cm::store::dataType::Policy |
integrationFromDocument(const json::Json&, bool requireUUID) | 把 JSON 对象转换为cm::store::dataType::Integration |
kvdbFromDocument(const json::Json&, bool requireUUID) | 把 JSON 对象转换为cm::store::dataType::KVDB |
requireUUID参数值得注意:在导入路径中一律传true(导入的文档必须自带 UUID),而在upsertResource路径中传false——因为单个资源的 upsert 允许不提供 UUID,由存储层分配,再按"UUID 是否存在则更新、否则按名创建"的分支处理(L582-L618)。
关键流程剖析
upsertResource:按类型分派的创建/更新决策
upsertResource(L571-L669)接收json::Json载荷后按ResourceType分支:
- INTEGRATION— 经
integrationFromDocument(resource, /*requireUUID:*/ false)转换,validateIntegration()验证后,若 UUID 非空且assetExistsByUUID(uuid)为真则updateResourceByUUID,否则createResource(按名创建)。 - KVDB— 经
kvdbFromDocument()转换后,采用同样的"按 UUID 存在性"决策。 - DECODER / FILTER / OUTPUT— 先经
detail::adaptDecoder/Filter/Output()适配,再校验"名字前缀与类型一致",然后validateAsset(),最后以名字作为幂等键:assetExistsByName(name)为真则updateResourceByName,否则createResource。
任一步失败都包装为带资源类型与命名空间上下文的std::runtime_error抛出。注意 integration/KVDB 以 UUID 为幂等键、资产以逻辑名为幂等键的差异,这与ResourceSummary中"UUID 在命名空间内唯一、name 为逻辑名"的注释一致。
importNamespace(JSON 文档重载)
前文"命名空间导入"一节已详述,其步骤与 README 完全对应:解析并严格校验文档结构 → 创建命名空间 → 注册bestEffortDelete回滚 → 按KVDBs → Decoders → Filters → Outputs → Integrations严格顺序导入 → 除非force == true否则逐项验证 → 成功后返回导入的Policy(L362:return store->getNS(nsId)->getPolicy();)。
getResourceByUUID:UUID 到 JSON 的解析路径
getResourceByUUID(L526-L569)分三步:
- 通过命名空间只读视图的
resolveNameFromUUID(uuid)把 UUID 解析为{name, type}; - 按类型加载类型化对象:INTEGRATION →
getIntegrationByUUID,KVDB →getKVDBByUUID,DECODER/OUTPUT/FILTER →getAssetByUUID; - 把类型化对象序列化回
json::Json返回(integration/KVDB 经各自的toJson(),资产直接返回视图层 JSON)。
validateResource:隔离验证的类型分派
validateResource(L686-L756):
- DECODER / FILTER:适配载荷 → 提取名字与 UUID(校验 UUIDv4)→ 校验名字前缀与类型一致 → 调
validator->validateAssetShallow()(浅验证,通过throwIfError把OptError转为异常)。 - INTEGRATION:经
Integration::fromJson(payload, /*requireUUID:*/ true)转换并校验base::Name::isValidPart名字合法性。 - KVDB:经
KVDB::fromJson(payload, /*requireUUID:*/ true)转换并做同样的名字合法性检查。
目录结构
cmcrud/ ├── CMakeLists.txt ├── README.md ├── interface/cmcrud/ │ └── icmcrudservice.hpp # ICrudService 纯虚接口 + ResourceSummary ├── include/cmcrud/ │ └── cmcrudservice.hpp # CrudService 具体实现头 ├── src/ │ └── cmcrudservice.cpp # 完整实现(约 800 行) └── test/ ├── mocks/cmcrud/ │ └── mockcmcrud.hpp # GMock mock(MockCrudService) ├── unit/ # 单元测试 └── component/ # 组件测试各文件在当前仓库中的实际路径为 接口、实现头、实现、mock。
CMake 构建目标
CMakeLists.txt 定义了五个目标,与 README 表格一致(并补充了yml私有依赖这一表格未列出的细节):
| Target | 类型 | Alias | 链接 |
|---|---|---|---|
cmcrud_icmcrud | INTERFACE | cmcrud::icmcrud | cmstore::icmstore(另含base) |
cmcrud_cmcrud | STATIC | cmcrud::cmcrud | cmcrud::icmcrud、builder::ibuilder(public),yml(private) |
cmcrud_mocks | INTERFACE | cmcrud::mocks | cmcrud::icmcrud、GTest::gmock |
cmcrud_utest | Executable | — | cmcrud::cmcrud、cmstore::mocks、builder::mocks、GTest::gtest_main |
cmcrud_ctest | Executable | — | cmcrud::cmcrud、cmstore::mocks、builder::mocks、GTest::gtest_main |
所有测试目标都在if(ENGINE_BUILD_TEST)条件内注册,并通过gtest_discover_tests向 CTest 暴露用例。
测试组织
- 单元测试(cmcrudservice_test.cpp,约 2500 行):使用 mock 的
ICMStore与IValidator逐方法测试CrudService。 - 组件测试(cmcrud_test.cpp,约 570 行):覆盖导入流程与资源生命周期的更宽场景。
- Mock(mockcmcrud.hpp):
MockCrudService用 GMock 宏实现了ICrudService的全部 13 个方法(含两个importNamespace重载),供下游消费者(如api/cmcrud、cmsync)的测试注入。
消费者与装配位置
| 模块 | 依赖 | 角色 |
|---|---|---|
api/cmcrud | cmcrud::icmcrud | HTTP API 处理器,向外部客户端暴露命名空间与资源的 CRUD 操作 |
cmsync | cmcrud::icmcrud | 内容同步服务,使用ICrudService从集群导入内容 |
main.cpp | cmcrud::cmcrud | 创建CrudService实例,把它与 store、validator 装配起来 |
装配点可以在引擎入口 main.cpp 中确认:
cmCrudService = std::make_shared<cm::crud::CrudService>(cmStore, builder);即CrudService在引擎启动时以 store 与 builder(validator)构造,随后注入到 API 服务器与同步服务(main.cpp 中 L826 附近将其作为依赖传入)。这也解释了前文反复出现的"弱指针"设计:main.cpp持有唯一的强引用,其余所有使用者(handlers、cmsync、CrudService自身的成员)都以weak_ptr间接持有,析构顺序由引擎入口统一掌控。
小结
cmcrud 是 Wazuh 引擎内容管理链条上的"守门人":向上以纯虚接口ICrudService隔离 API 层与同步服务,向下把持久化交给cmstore、把一致性判断交给builder::IValidator。它的三个核心机制——按类型分派的验证先于变更、资产适配保证的确定性序列化、带严格依赖顺序与 best-effort 回滚的命名空间导入——共同保证了cmstore中不存在结构不合法的工件;而贯穿全模块的weak_ptr+ 入口lock()模式,则把生命周期问题从"隐蔽的悬垂引用"变成了"显式且可诊断的运行时错误"。
【免费下载链接】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),仅供参考