ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Wazuh agent_info 模块数据库结构深度解析:SQLite Schema、元数据持久化与同步状态机

Wazuh agent_info 模块数据库结构深度解析:SQLite Schema、元数据持久化与同步状态机 Wazuh agent_info 模块数据库结构深度解析SQLite Schema、元数据持久化与同步状态机【免费下载链接】wazuhWazuh - The Open Source Security Platform. Unified XDR and SIEM protection for endpoints and cloud workloads.项目地址: https://gitcode.com/GitHub_Trending/wa/wazuhagent_info是 Wazuh agent 端负责采集、持久化并同步代理身份与元数据的核心模块。本文以其数据库结构文档 docs/ref/modules/agent_info/database-schema.md 为骨架结合仓库内 agent_info 模块源码 与 实现源码 的逐行印证系统讲解三张表的字段语义、ECS 映射、运行状态机与重启恢复机制帮助你理解元数据变更检测、组同步与完整性校验的底层数据基础。为什么 agent_info 需要一个本地 SQLite 数据库从源码结构看agent_info模块的核心工作循环位于AgentInfoImpl::start()agent_info_impl.cpp它周期性执行populateAgentMetadata()把采集到的代理身份、操作系统信息与组归属写入本地数据库。之所以要用数据库而不是简单的内存缓存是因为需要满足两个硬性要求变更检测change detection通过把当前采集值与库中已有值做比对识别出INSERTED / MODIFIED / DELETED三类差异并据此生成事件、触发后续同步这正是updateChanges()agent_info_impl.cpp所做的事情。重启恢复resume after restart模块必须记住上次同步到哪里了还有哪些变更没同步给 manager否则重启后要么丢变更要么重复全量同步。这个状态被持久化在db_metadata表中启动时由loadSyncFlags()agent_info_impl.cpp读回内存。数据库本身通过共享库 dbsync 驱动创建实例时显式指定HostType::AGENT、DbEngineType::SQLITE3与DbManagement::PERSISTENTagent_info_impl.cpp即持久化的 SQLite 文件数据库。数据库文件位置在非单元测试环境下数据库文件路径固定为agent_info.cpp#define AGENT_INFO_DB_DISK_PATH queue/agent_info/db/agent_info.db即 agent 安装目录下的queue/agent_info/db/agent_info.db。三张表通过GetCreateStatement()agent_info_impl.cpp拼装为一条 DDL 语句在 DBSync 初始化时执行CREATE TABLE IF NOT EXISTS因此数据库是首次启动即自建的无需手工建表。表一agent_metadata— 代理身份与操作系统信息agent_metadata表是模块的数据核心存储代理的唯一身份标识和操作系统属性。它始终只包含一行数据每次采集周期都被整体覆盖更新本质上是当前状态的快照。建表语句与字段说明文档给出的建表语句如下CREATE TABLE IF NOT EXISTS agent_metadata ( agent_id TEXT NOT NULL PRIMARY KEY, agent_name TEXT, agent_version TEXT, host_architecture TEXT, host_hostname TEXT, host_os_name TEXT, host_os_type TEXT, host_os_platform TEXT, host_os_version TEXT );各字段的语义及对应的 ECSElastic Common Schema映射如下MandatoryColumnData TypeDescriptionECS Mapping✔️agent_idTEXTThe unique ID of the agent (e.g., 001).agent.idagent_nameTEXTThe name of the agent.agent.nameagent_versionTEXTThe version of the Wazuh agent.agent.versionhost_architectureTEXTThe hardware architecture of the host (e.g., x86_64).host.architecturehost_hostnameTEXTThe hostname of the host machine.host.hostnamehost_os_nameTEXTThe name of the operating system (e.g., Ubuntu).host.os.namehost_os_typeTEXTThe type of the operating system (e.g., Linux).host.os.typehost_os_platformTEXTThe OS platform identifier (e.g., ubuntu).host.os.platformhost_os_versionTEXTThe version of the operating system (e.g., 22.04).host.os.version值得注意的版本差异文档展示的是 9 字段的精简 DDL而仓库中实际执行的建表语句agent_info_impl.cpp还额外包含两个字段即 11 字段const char* AGENT_METADATA_SQL_STATEMENT CREATE TABLE IF NOT EXISTS agent_metadata ( agent_id TEXT NOT NULL PRIMARY KEY, agent_name TEXT, agent_version TEXT, host_architecture TEXT, host_hostname TEXT, host_os_name TEXT, host_os_type TEXT, host_os_platform TEXT, host_os_version TEXT, cluster_name TEXT, cluster_node TEXT);;其中cluster_name与cluster_node记录代理所连接 manager 集群的名称与节点名来自握手handshake数据。这意味着实际落库的 schema 比参考文档更完整排查问题时应以源码为准。数据从哪来populateAgentMetadata 的采集链路populateAgentMetadata()agent_info_impl.cpp负责填充这张表数据来源可以归纳为四路agent_id / agent_name由readClientKeys()agent_info_impl.cpp读取client.keys文件首行按ID NAME IP KEY空格分隔格式解析出 ID 与名称。agent_version直接取编译期常量__wazuh_version。操作系统信息6 个host_*字段调用m_sysInfo-os()获取的 JSON 中依次取出architecture、hostname、os_name、os_type、os_platform、os_version。cluster_name / cluster_node优先通过m_handshakeQueryFunction向 agentd 实时查询握手数据仅在实时查询从未成功过如单元测试环境时才回退到模块启动时缓存的值。数据组装为单元素 JSON 数组后交给updateChanges(AGENT_METADATA_TABLE, ...)走 DBSync 事务落库并在回调里由categorizeMetadataChanges()agent_info_impl.cpp区分普通元数据变更仅 cluster_name 变更仅 cluster_node 变更三种情况用于决定后续走哪条同步路径。事件生成与 ECS 输出当 DBSync 检测到插入/修改/删除时processEvent()agent_info_impl.cpp会先把原始行映射成 ECS 格式再封装为 stateless 事件发出。ecsData()agent_info_impl.cpp实现了文档表格中ECS Mapping一列的具体逻辑agent_id→agent.idagent_name→agent.nameagent_version→agent.versionhost_architecture→host.architecturehost_hostname→host.hostnamehost_os_name→host.os.namehost_os_type→host.os.typehost_os_platform→host.os.platformhost_os_version→host.os.version一个值得关注的过滤逻辑是shouldGenerateStatelessEvent()agent_info_impl.cpp如果agent_metadata的 MODIFIED 事件只涉及 OS 相关字段host_architecture、host_hostname、host_os_name、host_os_type、host_os_platform、host_os_version这些由 Syscollector 上报则跳过 stateless 事件避免与 Syscollector 的事件重复。这也解释了为什么 OS 字段变化不会触发模块协调——它们被显式归类为不需要同步的噪声。表二agent_groups— 代理组归属agent_groups表记录代理所属的每一个组每行代表一条组分配。一个代理加入多个组时该表会有多行。建表语句与字段说明CREATE TABLE IF NOT EXISTS agent_groups ( agent_id TEXT NOT NULL, group_name TEXT NOT NULL, PRIMARY KEY (agent_id, group_name), FOREIGN KEY (agent_id) REFERENCES agent_metadata(agent_id) ON DELETE CASCADE );MandatoryColumnData TypeDescriptionECS Mapping✔️agent_idTEXTThe ID of the agent, linking toagent_metadata.agent.id✔️group_nameTEXTThe name of a group the agent belongs to.agent.groups两个设计要点复合主键(agent_id, group_name)保证同一代理不会重复记录同一组组集合的增删通过行的插入/删除来表达。外键级联删除FOREIGN KEY (agent_id) REFERENCES agent_metadata(agent_id) ON DELETE CASCADE—— 一旦agent_metadata中的代理行被删除例如代理被移除其所有组归属自动级联清理保证两表数据一致。这在源码建表语句中完全一致agent_info_impl.cpp。组数据从哪来握手优先、merged.mg 兜底populateAgentMetadata()中采集组的逻辑有明确的优先级agent_info_impl.cpp优先使用握手下发的组列表从agent_info_get_agent_groups()读取 agentd 握手时 manager 下发的 CSV 组列表按逗号切分。注意这是一次性消费——读取成功后立即调用agent_info_clear_agent_groups()清空缓存后续周期改从merged.mg读取这样组变更由 remoted 通过merged.mg推送才能被及时感知。回退到merged.mg文件readAgentGroups()agent_info_impl.cpp解析etc/shared/merged.mgWindows 下为shared\merged.mg。该文件有两种格式单组格式首行是#groupname且该值不是 8 位十六进制哈希时直接把它当作唯一组名多组格式首行是#hash_id8 位十六进制随后通过形如!-- Source file: groupname/agent.conf --的 XML 注释逐一提取组名。组数据同样以 JSON 数组形式进入updateChanges(AGENT_GROUPS_TABLE, ...)。需要强调的是即使组列表为空也会执行更新——用空数组清掉库中旧组确保代理被移出所有组这一状态能够正确落地agent_info_impl.cpp。在 ECS 输出上group_name会被映射为数组形式的agent.groupsagent_info_impl.cpp。表三db_metadata— 模块自身的运行状态db_metadata是 agent_info 模块的控制平面存放同步标志与完整性检查时间戳是重启后正确恢复的关键。它永远只有一行主键被CHECK (id 1)约束锁死。建表语句与字段说明CREATE TABLE IF NOT EXISTS db_metadata ( id INTEGER PRIMARY KEY CHECK (id 1), should_sync_metadata INTEGER NOT NULL DEFAULT 0, should_sync_groups INTEGER NOT NULL DEFAULT 0, last_metadata_integrity INTEGER NOT NULL DEFAULT 0, last_groups_integrity INTEGER NOT NULL DEFAULT 0, is_first_run INTEGER NOT NULL DEFAULT 1, is_first_groups_run INTEGER NOT NULL DEFAULT 1 );ColumnData TypeDescriptionidINTEGERPrimary key, always1.should_sync_metadataINTEGERA boolean flag (0or1) indicating ifagent_metadatachanges need to be synchronized.should_sync_groupsINTEGERA boolean flag (0or1) indicating ifagent_groupschanges need to be synchronized.last_metadata_integrityINTEGERA Unix timestamp of the last successful integrity check for theagent_metadatatable.last_groups_integrityINTEGERA Unix timestamp of the last successful integrity check for theagent_groupstable.is_first_runINTEGERA boolean flag that is true if the module is running for the first time with a new database.is_first_groups_runINTEGERA boolean flag that is true if agent groups are being populated for the first time.从源码看SQLite 并没有真正的布尔类型因此布尔语义用INTEGER 0/1表达读取时用! 0判定真值。这张表如何被读写围绕db_metadata有一组对称的读写方法构成了完整的持久化状态机读启动恢复loadSyncFlags()agent_info_impl.cpp在start()一开始被调用通过selectRows把 6 个状态字段全部读回内存成员变量m_shouldSyncMetadata、m_shouldSyncGroups、m_lastMetadataIntegrity、m_lastGroupsIntegrity、m_isFirstRun、m_isFirstGroupsRun。如果查询不到任何行首次启动、数据库刚创建则按首次运行初始化所有标志。写状态落盘updateDbMetadata()agent_info_impl.cpp把内存中当前状态原样写回id 1的那一行。它由setSyncFlag()、resetSyncFlag()、updateLastIntegrityTime()等操作在内部调用确保任何状态变化立即持久化。标志设置setSyncFlag(table, true)agent_info_impl.cpp在检测到元数据/组变化后把对应should_sync_*置 1。标志复位resetSyncFlag(table)agent_info_impl.cpp在同步成功后复位标志同时把对应的is_first_*置为 false——这样首次运行的语义只会触发一次。完整性时间戳shouldPerformIntegrityCheck()agent_info_impl.cpp比较当前 Unix 时间 −last_*_integrity是否达到integrityInterval首次时间戳为 0只初始化不检查由updateLastIntegrityTime()agent_info_impl.cpp负责更新并落盘。三张表如何协同从变更检测到同步的完整数据流理解了 schema 之后把三张表串起来看一遍主循环就能看到它们各自扮演的角色。start(interval, integrityInterval)的主循环agent_info_impl.cpp每interval秒执行一次逻辑如下采集与落库populateAgentMetadata()采集元数据与组通过 DBSync 写入agent_metadata和agent_groups回调中记录是否有变化。路由同步标志agent_info_impl.cpp普通元数据变化含 cluster_name 变化被元数据吸收的情况→ 置should_sync_metadata组变化或仅 cluster_name 变化且无其他元数据变化→ 置should_sync_groups仅 cluster_node 变化 → 不置任何标志被显式抑制。Delta 同步若should_sync_metadata置位且非首次运行执行performDeltaSync(AGENT_METADATA_TABLE)组同理。首次运行时跳过同步并直接复位标志因为首次全量数据会在随后的完整性检查中覆盖。Delta 同步内部由coordinateModules()agent_info_impl.cpp执行暂停 FIM/SCA/Syscollector → 获取版本 → 计算新版本元数据为 max1、组为 max→ 触发 flush → 恢复模块 → 等待 flush 完成 → 经AgentSyncProtocol同步给 manager的完整协调流程。完整性检查若对应标志未置位且距上次检查超过integrityInterval执行performIntegritySync()agent_info_impl.cpp对全部索引做轻量级对账无论结果如何都会刷新last_*_integrity时间戳。休眠等待下一个interval直到收到停止信号。这一流程保证了delta 同步保证变更实时送达完整性检查兜底保证长期一致性db_metadata保证任何时刻崩溃重启都不会丢失还有变更未同步这一事实。重启恢复与一致性保障得益于db_metadata的持久化模块的重启恢复路径非常清晰启动时loadSyncFlags()恢复内存状态若上次崩溃前刚设置了should_sync_metadata 1但未来得及同步重启后的第一个周期就会重新触发 delta 同步不会丢变更is_first_run/is_first_groups_run保证首次全量推送只在全新数据库上发生一次避免重复灌数据正常停机路径stop()agent_info_impl.cpp会等待运行循环退出、关闭 DBSync 连接并销毁同步协议确保 SQLite 文件处于一致状态。该行为在仓库的测试套件中有直接验证事件处理测试 agent_info_events_test.cpp 覆盖了agent_metadata与agent_groups的 INSERT/MODIFIED/DELETE 事件生成与 ECS 字段断言同步标志路由测试 agent_info_cluster_coordination_test.cpp 通过断言日志Set sync flag for agent_metadata to 1/Set sync flag for agent_groups to 1验证了不同变更组合下标志的正确路由完整性检查则在 agent_info_integrity_test.cpp 中覆盖。运维排查要点基于上述 schema 语义日常排查可以关注以下几点想确认数据库实际落库结构直接检查 agent 端的queue/agent_info/db/agent_info.db可用sqlite3执行.schema注意agent_metadata实际为 11 字段含cluster_name、cluster_node比参考文档多两列。元数据不更新确认interval配置与client.keys可读性agent_id/agent_name缺失时模块会记录 WARNING相关配置见 docs/ref/modules/agent_info/configuration.md 中的agent-info块。同步反复失败观察db_metadata.should_sync_metadata/should_sync_groups是否长期为 1若协调被 FIM 首次同步阻塞模块会按周期重试CoordinationResult::Deferred此时同步标志会保留到下一个周期继续尝试。组归属异常结合握手组列表与merged.mg两种数据源判断——握手组只在启动后第一个采集周期消费一次之后的组变更来自merged.mg。三张表的分工可以总结为一句话agent_metadata存代理是谁、系统是什么agent_groups存代理属于哪些组db_metadata存模块自己进行到哪一步、接下来要干什么。理解了这个数据模型就掌握了 agent_info 模块变更检测、协调同步与故障恢复的全部底层机制。【免费下载链接】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),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进