从零构建SOC:Wazuh + IRIS-web 真实项目实战报告

历时三周、逐条commit的实录:使用Wazuh 4.x、IRIS-web和自研FastAPI集成器从零搭建生产级安全运营中心——检测规则、告警流水线、IOC情报富化,以及那些永远不会出现在架构图里的基础设施坑点。

技术栈: Wazuh 4.x · IRIS-web · soc-integrator (FastAPI) · OpenSearch · Docker Compose · VirusTotal API · AbuseIPDB

flowchart TD
  subgraph "Log Sources"
    A["Windows Agent"]
    B["FortiGate Syslog"]
    C["Simulator Scripts"]
  end

  subgraph "Wazuh"
    D["Wazuh Manager"]
    E["Decoders + Rules"]
    F["OpenSearch Indexer"]
  end

  subgraph "soc-integrator (FastAPI)"
    G["Alert Poller"]
    H["Severity Filter"]
    I["IOC Enricher"]
    J["WazuhSyslogAdapter"]
    K["Webhook Receiver"]
    L["Email Notifier"]
  end

  subgraph "External Threat Intel"
    M["VirusTotal"]
    N["AbuseIPDB"]
    O["Feodo / URLhaus / ThreatFox"]
  end

  subgraph "IRIS-web"
    P["IRIS Alerts"]
    Q["Cases + Triage"]
    R["Outbound Webhook"]
  end

  A --> D
  B --> D
  C --> D
  D --> E
  E --> F
  F --> G
  G --> H
  H --> I
  I --> M
  I --> N
  O --> I
  I --> P
  P --> Q
  Q --> R
  R --> K
  K --> L
  J --> D
  I --> J

第一周:从一张白纸开始(3月13日–16日)

每个SOC项目都以同样的方式开始:光标在屏幕上闪烁,面前是一份需要检测的威胁场景清单。

我们的起点是一份内部需求规格书——三个用例附录:

  • 附录A — Windows 与 Active Directory 滥用
  • 附录B — 网络与防火墙事件(FortiGate)
  • 附录C — 身份安全:impossible travel、credential abuse、管理员横向移动

最初的commit毫不起眼:添加multipart依赖、提交三个附录的样本日志文件——137行来自接近生产环境的系统收集的防火墙、认证和Windows事件数据。随后,一个pass.txt出现在仓库里:早期SSH会话的凭据笔记和测试输出。

3月16日,第一个真正的里程碑达成:test-firewall-syslog.py脚本——能够向Wazuh的514端口发送FortiGate格式的UDP syslog数据包,覆盖10种不同场景。

Docker NAT问题

--via-docker标志几乎是立即加入的——第二天就发现了这个问题。没有它,每个数据包到达Wazuh时,源IP都是Docker网关IP而非实际主机IP,导致基于源IP的规则匹配完全失效。该标志强制数据包通过宿主机网络栈路由,让Wazuh看到正确的来源IP。

如果你正在Docker内构建Wazuh模拟环境,却发现源IP规则从不触发——原因就在这里。

flowchart TD
  A["test-firewall-syslog.py"]
  B{"--via-docker flag?"}
  C["Packet exits via Docker NAT"]
  D["Wazuh sees Docker gateway IP"]
  E["Source-IP rules never match"]
  F["Packet exits via host network"]
  G["Wazuh sees real host IP"]
  H["Source-IP rules match correctly"]

  A --> B
  B -- "No" --> C
  C --> D
  D --> E
  B -- "Yes" --> F
  F --> G
  G --> H

第二周:规则、解码器与OR陷阱(3月17日–22日)

检测工程,说白了就是不断重写你以为已经写对的东西。

3月17日涌现出一批带有progressprogress-updaterule update标签的commit。附录A、B、C共59条模拟规则正在成形——但有些不对劲:A2和A3规则在不该触发的时候触发了。

根本原因:多<match>的OR陷阱

Wazuh的XML规则语法允许在单条规则内写多个<match>标签。大多数工程师默认这是AND逻辑。但在Wazuh 4.x的某些解码器链中,它实际表现为OR

一条本应只在action=deny logid=13同时出现时才触发的规则,有时仅满足其中一个条件就会触发。

<!-- 错误:在某些解码器链中表现为OR -->
<rule id="100201" level="10">
  <match>action=deny</match>
  <match>logid=13</match>
  <description>Firewall block — specific log ID</description>
</rule>

<!-- 正确:用单条regex强制实现AND逻辑 -->
<rule id="100201" level="10">
  <regex>action=deny.*logid=13|logid=13.*action=deny</regex>
  <description>Firewall block — specific log ID</description>
</rule>

3月22日的commit用一句话描述了修复:

"fix A2/A3 rule OR-trap: replace multi-<match> with single <regex> lookaheads."

这一行commit背后,是整整一个下午反复用wazuh-logtest喂入样本事件、眼睁睁看着规则对本该忽略的输入做出响应的排查过程。如果你的Wazuh规则触发范围比预期宽泛,检查一下同一条规则内是否存在多个<match>标签。

flowchart TD
  subgraph "WRONG: multi-match behaves as OR"
    A1["Incoming log event"]
    B1["match: action=deny"]
    C1["match: logid=13"]
    D1["Rule fires if EITHER matches"]
    A1 --> B1
    A1 --> C1
    B1 --> D1
    C1 --> D1
  end

  subgraph "CORRECT: single regex enforces AND"
    A2["Incoming log event"]
    B2["regex: action=deny AND logid=13"]
    C2["Rule fires only when BOTH match"]
    A2 --> B2
    B2 --> C2
  end

第三周:打通Wazuh与IRIS的链路(3月23日–25日)

让Wazuh规则触发并在日志文件里看到结果,是一回事。把告警送入分析师可以进行研判的工单管理平台,是完全不同的另一个问题。

soc-integrator流水线

答案是soc-integrator——一个坐落在Wazuh与IRIS-web之间的FastAPI服务:

flowchart TD
  A["Raw Security Event"]
  B["Wazuh Manager"]
  C["Decoder Chain"]
  D["Rule Matching"]
  E["OpenSearch Indexer"]
  F["soc-integrator Poller"]
  G{"Severity >= threshold?"}
  H["Discard (noise)"]
  I["Create IRIS Alert"]
  J["IRIS-web Case Queue"]

  A --> B
  B --> C
  C --> D
  D --> E
  E --> F
  F --> G
  G -- "No" --> H
  G -- "Yes" --> I
  I --> J

集成器的核心功能:

  • 每隔N秒轮询Wazuh Indexer(OpenSearch)
  • 仅转发达到或超过可配置严重级别阈值的告警(默认:medium及以上——低于此的均视为噪声)
  • 提供GET/PUT接口,允许分析师在不重启服务的情况下实时调整阈值
  • 生成携带完整事件元数据的结构化IRIS告警

3月23日,一个7步端到端测试脚本验证了从原始事件到IRIS告警的完整链路。

一天内修复三处时区问题

所有容器都在跑UTC。身处曼谷(ICT,UTC+7)的分析师看到的时间戳偏差了七小时。

  • 标准容器:在Docker Compose的env块中添加TZ=Asia/Bangkok
  • Go/scratch基础镜像:构建时时区数据库被裁剪掉了——需要以volume形式显式挂载
  • 附加修复:在IRIS导航栏添加ICT/UTC双时钟小组件

分析师立刻就注意到了。小改动,实际影响不小。


IOC情报富化与弃用Shuffle SOAR

3月24日之前,威胁情报还是纯手工操作——分析师手动查询可疑IP。新的IOC流水线彻底取代了这一方式:

按需查询:

  • VirusTotal——IP/域名/哈希信誉查询
  • AbuseIPDB——IP滥用历史记录

后台订阅源摄入:

Wazuh CDB列表文件(malicious-ipmalicious-domainsmalware-hashes)通过Wazuh API重新生成并热重载。新增规则110600–110602负责内联CDB匹配。

flowchart TD
  subgraph "Ad-hoc Lookups"
    A["Suspicious IP / Domain / Hash"]
    B["VirusTotal API"]
    C["AbuseIPDB API"]
    A --> B
    A --> C
  end

  subgraph "Background Feed Ingestion"
    D["Feodo Tracker (C2 IPs)"]
    E["URLhaus (Malicious URLs)"]
    F["ThreatFox (IOC Aggregator)"]
    G["MalwareBazaar (Hashes)"]
  end

  subgraph "Wazuh CDB Hot-Reload"
    H["malicious-ip list"]
    I["malicious-domains list"]
    J["malware-hashes list"]
    K["Wazuh API reload trigger"]
    H --> K
    I --> K
    J --> K
  end

  subgraph "Detection Rules"
    L["Rule 110600: IP match"]
    M["Rule 110601: Domain match"]
    N["Rule 110602: Hash match"]
  end

  B --> H
  C --> H
  D --> H
  E --> I
  F --> H
  F --> I
  G --> J
  K --> L
  K --> M
  K --> N

Shuffle SOAR被彻底移除。 直接调用API更快、更简单,也不需要维护一个独立的工作流平台。

私有IP泄漏

早期测试中,集成器将192.168.x.x10.x.x.x地址提交给了VirusTotal,消耗API配额并因内部扫描流量触发大量429限速错误。

解决方案:在调用任何外部富化接口前,先过滤掉RFC1918和回环地址。调用外部威胁情报API之前,务必检查并排除私有IP。


磁盘告急、仪表板一无所显、同步噪声

logall_json的灾难

开发调试期间开启的logall_json配置,在生产环境中每天写入14GB的archives.json

解决方案:关闭logall_json;应用OpenSearch ISM策略,30天后自动删除旧索引;在容器内OS层面添加日志轮转。

什么都不显示的仪表板

附录C仪表板的过滤条件是rule.id:1005*——开发阶段的模拟规则ID范围。而生产环境中真实的检测规则落在110xxx区间,仪表板因此对真实事件一无所返。

解决方案:从规则ID过滤切换为rule.groups:appendix_c。基于分组的过滤方式不会受规则ID变动影响。

同步过滤器中的事件类型不匹配

Wazuh→IRIS同步曾依赖event_type文本字段做过滤,而这个字段在Wazuh agent产生的真实Windows事件中根本不存在——它只是模拟阶段的产物。

解决方案:将过滤器重构为显式规则ID的frozenset。明确、确定、易于代码审查。


第四周:Webhook、邮件通知与闭环(3月28日)

3月28日是整个项目密度最高的一天。

IRIS Webhook接收器

当IRIS创建或更新告警时,值班分析师如何得知?答案是在soc-integrator中实现一个webhook接收器。IRIS通过其模块系统支持出站webhook。

flowchart TD
  A["IRIS Alert created or updated"]
  B["IRIS outbound webhook module"]
  C["POST /iris/webhook (soc-integrator)"]
  D["Parse alert payload"]
  E["Enrich: entity name + event type"]
  F["Resolve IRIS_EXTERNAL_URL"]
  G["Format email body"]
  H["smtplib send"]
  I["On-call analyst inbox"]

  A --> B
  B --> C
  C --> D
  D --> E
  E --> F
  F --> G
  G --> H
  H --> I

邮件通知在一天内经历了三次迭代:

版本 变更内容
v1 仅简单通知:"有事件到达"
v2 主题明确化:"A1-02 Brute Force"而非"IRIS Event"
v3 完整上下文:告警ID、标题、关联工单、直达链接

.env内联注释解析Bug

IRIS_EXTERNAL_URL=http://10.0.0.5 # production host被整体解析为http://10.0.0.5 # production host。环境变量行末的内联注释悄无声息地污染了变量值。

解析.env文件时务必剥离内联注释,或使用能正确处理此类情况的解析器,例如Python的python-dotenv

模拟与生产的解码器分叉

来自Windows Wazuh agent的真实事件使用windows_eventchannel解码器链;通过syslog注入的模拟事件使用JSON解码器。两者完全互斥——挂载在windows_eventchannel链上的规则永远不会响应模拟事件。

flowchart TD
  A["Incoming Windows Event"]
  B{"Source type?"}
  C["Real Wazuh Agent"]
  D["Syslog-injected Simulator"]
  E["windows_eventchannel decoder"]
  F["JSON decoder"]
  G["Production anchor rule"]
  H["Simulation anchor rule 100270"]
  I["16 A4 Production rules"]
  J["16 A4 Simulation rules"]

  A --> B
  B -- "Agent" --> C
  B -- "Simulator" --> D
  C --> E
  D --> F
  E --> G
  F --> H
  G --> I
  H --> J

解决方案:为JSON解码器路径创建锚规则(100270),将A4的16条模拟规则全部指向该锚规则,而非生产锚规则。

JSON格式化是检测流水线的一部分

Windows事件的告警描述在IRIS中以单行压缩JSON呈现。需要在两处同时修复:

  1. 集成器侧:检测到JSON字符串后,在写入IRIS前以2空格缩进格式化
  2. 前端侧<span>元素会折叠空白字符——改用<pre style="white-space:pre-wrap">,格式化后的JSON才能正确渲染

以单行压缩JSON呈现的告警描述会被忽略;同样的数据加上缩进,才会被阅读和处理。呈现方式是检测流水线的组成部分。


第五周:闭合反馈环路与消灭误报(3月31日–4月1日)

闭合Wazuh反馈环路

C系列检测(C1 impossible travel、C2 credential abuse、C3 lateral movement)的检测逻辑运行正确,但处于"沉默"状态——集成器确认匹配并创建IRIS告警,而Wazuh本身对此毫不知情。

这带来两个实际问题:

  1. Wazuh中的level 15规则应在攻击被确认后触发,而不只是对原始事件响应
  2. Wazuh不知道确认检测发生了,SOC仪表板就无法如实展示

解决方案:WazuhSyslogAdapter——集成器内部一个小巧的UDP发送器。确认C1匹配后,集成器将结构化syslog事件回送至Wazuh:

soc_event=correlation event_type=c1_impossible_travel user="..." src_ip=...

Wazuh通过soc-prod-integrator解码器解析,触发锚规则100260,最终以level 15(已确认严重)触发规则110502。环路闭合,仪表板如实呈现。

flowchart TD
  A["Login event from two locations"]
  B["Wazuh raw rule fires (low level)"]
  C["OpenSearch Indexer"]
  D["soc-integrator poller"]
  E["C1 correlation logic"]
  F{"Impossible travel confirmed?"}
  G["Discard"]
  H["Create IRIS Alert"]
  I["WazuhSyslogAdapter"]
  J["UDP syslog back to Wazuh port 514"]
  K["soc-prod-integrator decoder"]
  L["Anchor rule 100260"]
  M["Rule 110502 fires (level 15 critical)"]
  N["SOC dashboards updated"]
  O["Email notification sent"]

  A --> B
  B --> C
  C --> D
  D --> E
  E --> F
  F -- "No" --> G
  F -- "Yes" --> H
  H --> I
  I --> J
  J --> K
  K --> L
  L --> M
  M --> N
  H --> O

非工作时间检测的时区错误

C2的非工作时间检测窗口被配置为20:00–06:00 UTC。听起来合理——直到换算成本地时间:

  • 20:00 UTC = 03:00 ICT(曼谷)
  • 曼谷工作时间从08:00 ICT = 01:00 UTC开始

规则在正常上班时间段内持续触发。

修正后的窗口:11:00–01:00 UTC = 18:00–08:00 ICT。时间类检测规则务必先按分析师所在时区设定,再转换为UTC。

C3-03:每日24次误报 → 归零

规则C3-03检测通过RDP type-3登录实施的管理员横向移动,但每天在FPBIADFS01上触发24次。

根本原因:AD FS持续执行无源IP的服务间type-3认证——完全符合规则的检测模式,但来自已知安全的服务账号。

修复:添加单一守卫条件——ipAddress必须存在且非回环地址。AD FS服务认证没有源IP,因此被干净过滤掉;真实横向移动携带远程源IP,仍会正常触发规则。

flowchart TD
  A["Type-3 logon event detected"]
  B{"ipAddress present and non-loopback?"}
  C["FPBIADFS01 service auth (no IP)"]
  D["Rule suppressed — false positive avoided"]
  E["Remote admin session (real IP)"]
  F["C3-03 fires — lateral movement alert"]

  A --> B
  B -- "No" --> C
  C --> D
  B -- "Yes" --> E
  E --> F

一个条件。误报率:归零。


没有写进任何Commit的基础设施备忘

macOS bind-mount的inode问题

Docker on macOS通过inode追踪bind-mount的文件。当编辑器在保存时创建新inode(sed -i或部分IDE的常见行为),容器仍从旧inode读取。症状:修改Wazuh规则、重载Wazuh、测试——旧行为依然存在。

修复:在macOS上修改bind-mount的配置文件后,必须执行docker compose up --force-recreate。第一天就写进README。

wazuh-logtest stdin挂起

通过docker exec -i将20行测试文件pipe进wazuh-logtest,会在最后一个事件的phase 2之后卡住。

修复:将测试内容写入容器内的临时文件后重定向,或给命令调用加上超时。

通过docker exec传输大文件

部分主机上通过docker exec传输大文件时,pipe在约64KB处被截断。

修复:在宿主机对文件进行base64编码,将编码后的字符串pipe进容器,在容器内用Python解码。


当前状态

截至2026年4月1日

模块 状态
附录A — Windows/AD模拟 59条规则,全部端到端触发验证通过
附录B — 网络/防火墙 生产规则运行中,FortiGate syslog正常接入
附录C — 身份安全(C1/C2/C3) 检测 + Wazuh反馈环路已闭合
Wazuh → IRIS同步 运行中,按严重级别和规则ID双重过滤
IOC情报富化 VT + AbuseIPDB + 4个威胁订阅源,CDB热重载
邮件通知 每次IRIS webhook事件触发,携带完整告警上下文
误报控制 C3-03 ADFS守卫、C2时区修正、登录类型过滤
日志保留 Wazuh 3天,OpenSearch ISM策略30天

核心经验

1. 检测工程是迭代,不是积累

添加规则的commit和修复其误报的commit同等重要。一条对所有事件都触发的规则,比没有规则更糟——它会训练分析师忽略告警,从根本上动摇SOC存在的意义。

2. 反馈环路至关重要

进了IRIS但没有回写到Wazuh的检测,只是半个检测。SIEM必须知道关联引擎确认了什么,否则仪表板在说谎,分析师对平台的信任会逐渐瓦解。

3. 小的基础设施Bug会累积成大问题

时区错误、.env内联注释解析Bug、向VirusTotal提交私有IP——这些单独看都不会搞垮演示。但在生产环境中,它们制造持续的背景噪声,侵蚀团队对平台的信心。趁早修,别等它们变成"已知问题"后被默默忽视。

4. 始终检查Wazuh规则中的OR陷阱

单条Wazuh规则中的多个<match>标签,在特定解码器链上下文中可能表现为OR逻辑。如果规则触发范围超出预期,将其整合为单条带有明确AND逻辑的<regex>

5. JSON格式化不是锦上添花

以单行压缩JSON呈现的告警描述,会被快速扫过然后忽略。同样的数据加上合适的缩进,才会被认真阅读并触发行动。呈现方式是检测流水线不可或缺的一环。


延伸阅读


作者:Simplico Co., Ltd. — 总部位于曼谷的软件工程与产品工作室,专注于AI/RAG系统、网络安全/SOC解决方案及企业系统集成,服务泰国、日本、中国及全球英语市场。

技术栈:Wazuh 4.x · IRIS-web · soc-integrator (FastAPI) · OpenSearch · Docker Compose · macOS开发环境


Get in Touch with us

Chat with Us on LINE

iiitum1984

Speak to Us or Whatsapp

(+66) 83001 0222

Related Posts

Our Products