Files
graylog-ops/README.md
T
DeepFal 2d76ed6672 初始提交:恶意 IP 特征+行为融合检测系统
- 特征检测(规则/签名)与行为检测(24h 统计)单进程运行
- 融合名单按首次发现时间倒序,展示位置/归属(GeoIP + ASN)
- 钉钉融合推送(加签、24h 节流、去重)
- 页面状态灯、CSV 导出、Docker 镜像构建文件
2026-10-18 05:03:05 +08:00

34 KiB
Raw Blame History

graylog-ops

Graylog 4.0.x 只读信息采集 + 封禁判断工具,服务于"用业务访问日志自动封 IP"这条链路。

边界(先说清楚)

Graylog 是旁路日志系统:它能看到、能聚合、能告警,但不在业务流量链路上,不能自己封 IP。 本工具同样只读:不建流、不改索引、不建事件、不下发任何封禁。

外部客户端
   │
   ▼
深信服 AD 7.0.8R5(应用交付 / 负载均衡 / 反代)
   │        ▲
   │        └── 将来封禁的执行点大概率在这里(黑名单 / 访问控制)
   ▼
业务系统(nginx 等) ──日志──► Graylog ──► graylog-ops(本工具:采集 + 判断)
                                              │
                                              └──► {ip, action, score, reasons}

深信服 AD 在链路里的影响(关键)

AD 挡在业务系统前面,会直接改变"日志里的客户端 IP"的含义,排查顺序:

  1. AD 是否透传真实客户端 IP:HTTP 类型的业务看 AD 的"真实客户端 IP / X-Forwarded-For"配置; 四层转发通常保留源地址,七层反代不配置就会变成 AD 自己的地址。
  2. 后端 nginx 的 $remote_addr 是不是 AD 地址。若是,日志里必须加 $http_x_forwarded_for(或 AD 提供的专用头),并在采集端落成独立字段。
  3. 用 discover 验证:如果候选字段的"私有 IP 占比"很高、且 source 只有一个取值, 基本可以判定拿到的是 AD/反代地址,而不是真实客户端。
  4. 把 AD 的管理地址、业务 VIP、回源地址写进白名单,避免把自己的设备封掉。

为什么先做"信息获取 + 判断"

封禁规则依赖四个事实,猜错任何一个都会封错人:

  1. 真实客户端 IP 落在哪个字段(source 往往是采集器/反代地址,不是访问者)
  2. 日志里有哪些可做判据的字段(status / request_uri / user_agent / response_time …)
  3. 量级(每秒多少条,决定窗口和阈值)
  4. 现有 stream / 索引 / 事件定义是怎么组织的

discover 回答 1、2;histogram / top-ips 回答 3;snapshot / streams / alerts 回答 4; judge 把这些聚合成"封谁、为什么、怎么处理"。

快速开始

零第三方依赖,只用 Python 3.7+ 标准库。

cd graylog-ops

# 方式 A:环境变量
export GRAYLOG_URL=http://edu-master:9002
export GRAYLOG_TOKEN='从 Graylog UI 创建的 API token'
bin/graylog-ops probe

# 方式 B:配置文件(推荐,token 不落 shell history)
cp config.example.json graylog-ops.json
# 编辑 graylog-ops.json 填 url / token
bin/graylog-ops --config graylog-ops.json probe

# 方式 C:临时参数
bin/graylog-ops --url http://edu-master:9002 --token "$TOKEN" probe

认证方式:API token 当用户名,字面量密码 token(4.0 文档的写法)。 工具内部已按此拼 Authorization: Basic base64(<token>:token),你只需要给 --token。 非 GET 请求会自动带 X-Requested-By(Graylog 的 CSRF 保护要求)。

token 放哪:写进 graylog-ops.json(已在 .gitignore 里)并 chmod 600。 只需要读权限的 token,不需要管理员账号密码;用完在 UI 里一键吊销。 不要把 token 贴到聊天、工单或截图里——--config 读文件时它不会出现在命令行或输出中。

命令

命令 作用
probe 连通性 / 认证 / 16 个接口的可用性矩阵,第一步跑这个
system 版本、节点、索引器健康、deflector、license
streams [--rules] stream 列表(分流与权限边界),--rules 额外拉规则
inputs input 类型、监听地址、端口
fields Graylog 已注册字段及类型
indices index set 轮转/保留策略、索引大小
alerts 现有事件定义与通知方式(改造前先看,别重复建)
discover 采样日志,推断真实客户端 IP 字段(核心)
judge 把聚合信号判成封禁候选(只读,dry-run 语义)
top-ips terms 聚合,默认 --field client_ip,看 top IP 与计数
terms 任意字段的 terms 聚合
messages 拉原始消息(表格 / --jsonl)
histogram 消息量时间分布,估算阈值与容量
snapshot 全量环境信息落盘(每个资源一个 JSON + manifest)
raw 直接调用任意 API 路径(支持 --method/--body),应对版本差异
collect 一条命令产出 probe/discover/judge/snapshot 整套信息
watch 持续检测恶意公网 IP + SQLite 清单 + 本地 Web 面板

通用参数:--json(机器可读)、--out FILE(写 JSON)、-v(打印每个 HTTP 请求)。

五步摸底

# 1) 能不能通、这个版本到底有哪些接口
bin/graylog-ops probe

# 2) 客户端 IP 在哪个字段(最关键的结论),同时生成 profile
bin/graylog-ops discover --range 3600 --stream 业务访问日志 \
    --save-profile profiles/edu.json

# 3) 用真实数据看阈值是否合理
bin/graylog-ops top-ips --field client_ip --query 'status:401' --range 300 --size 30
bin/graylog-ops histogram --range 3600 --interval 5m

# 4) 判断:谁该封、为什么
bin/graylog-ops judge --profile profiles/edu.json --range 300 \
    --allow allowlist.example.txt --explain 5

# 5) 整体快照存档,方便离线分析和复盘
bin/graylog-ops snapshot --dir snapshots/$(date +%F) --sample 500

机器上要能同时访问 10.180.x(Graylog)和 10.230.x(AD),否则先在跳板机跑。

discover 的输出(示例数据):

采样 8 条消息  query=*  range=3600s  stream=(all)

分数  字段       出现率  IP占比  不同IP  私有IP  IP版本  样例
----  ---------  ------  ------  ------  ------  ------  ----------------------------
95.0  client_ip    100%    100%       4     12%  v4      203.0.113.5, 198.51.100.7
45.0  source       100%    100%       1    100%  v4      10.180.254.17

推荐字段: client_ip  (confidence=high)
  - 字段 `client_ip` 的 IP 占比 100%,样例 203.0.113.5, 198.51.100.7, 192.0.2.44
  ! `source` 只有 1 个不同取值,而 `client_ip` 有 4 个:`source` 是采集器/反代,封禁必须用 `client_ip`

判断规则(judge)

固定的 5 个 terms 查询 + 3 个统计量,不随 IP 数量增加查询次数:

信号 查询(字段名来自 profile) 权重 饱和点 说明
auth_fail status:(401 OR 403) 45 10 次 认证失败
scanner_ua http_user_agent:(sqlmap OR nikto OR nuclei OR nmap OR …) 50 2 次 扫描器特征
sensitive_path request_uri:(wp-login.php OR phpmyadmin OR actuator OR …) 25 5 次 敏感路径探测
not_found status:404 20 20 次 目录/文件探测
bad_method request_method:(PUT OR DELETE OR TRACE OR CONNECT) 10 5 次 异常方法
rate 请求数 / 200 25 200 次 绝对量
spike 请求量 ≥ 候选中位数 4 倍 15 64 倍 相对异常
auth_ratio 失败占比 ≥ 50% 且 ≥ 10 次 25 — 几乎全是失败

动作映射:

条件 动作
有攻击证据 且 score ≥ 75 ban_1h
有攻击证据 且 score ≥ 55 ban_30m
score ≥ 40 observe(只记录,不封)
其余 ignore

三条安全约束(比阈值本身更重要):

  1. 内网地址默认硬排除(RFC1918 / 回环 / 链路本地 / CGNAT),避免把 AD、LB、NAT 出口封掉; --include-internal 可关闭,但一般不该关。
  2. 白名单硬排除:--allow 支持多个文件,一行一个 IP/CIDR,# 注释。 把 AD 管理地址、业务 VIP、回源地址、办公出口、监控探针都写进去。
  3. 纯流量不封(require_evidence):没有任何攻击特征、只是请求量大的客户端, 最多判 observe,不会因为"量大"被自动拉黑。

judge 输出示例:

窗口 300s  query=*  field=client_ip  stream=(all)  候选池 4(中位请求数 35.0)

得分  动作    客户端 IP     请求数  依据
----  ------  ------------  ------  -----------------------------------------------
98.1  ban_1h  192.0.2.99        25  扫描器 UA 25 次; 敏感路径 25 次; 404 探测 25 次
75.0  ban_1h  203.0.113.66      40  认证失败 40 次; 失败请求占 100%

已排除(不参与判分):
  10.180.5.5           30     内部地址(RFC1918/回环/CGNAT)

信号命中 0 个 IP 时要注意:可能是该窗口确实没有这类流量,也可能是查询模板跟字段分词不匹配 (比如 request_uri 在 ES 里的分词方式和你以为的不一样)。用 profile 覆盖 query 后再跑, judge 会把每个信号的实际查询和命中数都打出来。

持续检测服务(watch)

# 持续运行(面板 + 检测循环)
bin/graylog-ops --config graylog-ops.json watch \
    --stream F5_Accesslog --profile profiles/f5.json \
    --interval 60 --window 900 --db data/ips.db \
    --port 8088 --allow allowlist.example.txt

# 只跑一轮(离线分析,比如 8 小时)
bin/graylog-ops --config graylog-ops.json watch --once \
    --stream F5_Accesslog --profile profiles/f5.json \
    --window 28800 --db data/ips-8h.db

# 只提供面板,浏览已有数据库
bin/graylog-ops --config graylog-ops.json watch --serve-only \
    --db data/ips-8h.db --port 8089
  • 面板:http://127.0.0.1:8088(30 秒自动刷新)
  • JSON API:/api/ips?tier=malicious&limit=100、/api/stats、/api/ip/<ip>
  • 存储:data/ips.db(SQLite)。IP 连续 --forget 秒(默认 7200)不再出现就自动移除
  • 名单:--export PATH 每轮把高置信恶意 IP 写成纯文本(默认剔除 shared_egress)

面板(给同事看的)

只有一个地址 http://<本机IP>:8088(只读,无鉴权,仅限内网使用)。 历史(17–18 日 25 小时)与实时检测共用同一个 SQLite(data/ips.db), 所以打开就是一个完整的大列表:

  • 核心名单(默认视图):只列高置信恶意 IP,默认按首次攻击倒序(最新在前),点任意一行进详情
  • 所有判定为恶意的 IP(含此前标为「疑似共享出口」的)统一归入核心名单,不再有「待确认」档
  • 详情页:评分环、请求总量、平均频率、峰值时段、活跃时段数、命中规则与次数、 请求频率分布图、状态码分布、请求方法分布、访问域名分布,以及按判定类型抽样的证据 (优先抽攻击类请求;高流量 IP 不会把日志刷屏),附处置建议
  • 搜索框可按 IP 或判定原因过滤;排序字段与方向是两个独立下拉(首次攻击 / 最近攻击 / 评分 / 请求数 × 升序 / 降序)
  • 单击任意 IP 地址即可复制(列表和详情页标题都支持,带 toast 提示)
  • 列表 30 秒自动刷新(可关闭)

界面截图:列表页 · 详情页

  • 一键导出(右上角两个下拉,都作用于当前筛选/排序结果):
    • 「复制 IP ▾」→ 逗号分隔 / 换行分隔(换行版可直接粘贴给 ipset、防火墙批量导入)
    • 「导出 ▾」→ CSV(完整字段) / 仅 IP 列表(每行一个)
  • 只读 API:/api/ips?scope=core|all、/api/ip/<ip>、/api/stats
  • 导出接口(脚本可直接拉):
    • GET /api/export.csv?scope=core|all&sort=&order= → CSV(含等级/评分/首次攻击/原因)
    • GET /api/export.txt?scope=... → 纯 IP 列表,每行一个

证据完全按需生成,本地库只存判定结果(56 个 IP 约 36 KB),点开详情时才连 Graylog:

  • 9 个查询并发(URI/UA/状态码/方法/域名/服务端 IP/VIP + 时间分布 + 样本),首次打开约 0.3–0.5 秒, 结果在内存缓存 10 分钟,再打开约 1 毫秒;进程重启后重新拉取
  • 样本走 views 的 messages 搜索:旧版 /search/universal/relative 在 25 小时窗口 + 复合查询下 会被 Graylog 断连,表现就是"只有分布没有样本"(WebShell 这类 IP 尤其明显)
  • 样本按判定类型分组:每种类型各保留最近 3 条(两种类型 6 条、三种类型 9 条,以此类推), 同一秒内的重复请求会合并;没有命中规则时退化为最近 5 条通用请求
  • 每条样本展示源日志的完整字段:客户端/服务端 IP:端口、Host、VIP(bigip)、VS、 地区、响应时间 rt、请求长度、Referer、采集来源,以及原始日志行
  • 长字段可点击弹窗查看完整内容(URI / UA / Referer / Host / 原始日志行),Esc 或点遮罩关闭
  • 轻量快照兜底:每次查看详情时顺手存一份 1–2 KB 的摘要(URI/状态/方法/VIP/时间分布), Graylog 索引轮转(约 10 天)后自动回退到快照,并明确标注"日志已过期,以下为判定时的快照"
  • 负载:每轮固定 6 个 terms 查询 + 最多 --verify 次核验,与 IP 数量无关 (本环境 15 分钟窗口约 3.5–5 秒/轮)

规则(速率类信号按窗口归一化到 15 分钟基准,行为类信号不缩放):

规则 权重 饱和 说明
敏感路径探测 65 2 次 /actuator、/.env、/.git、/wp-login、/druid、/nacos、/swagger、/console、/ISAPI…
扫描器 UA 55 1 次 sqlmap / nikto / nuclei / nmap / zgrab …
登录失败(401/403) 55 8 次 登录类 URI 上的认证失败
爬虫 UA 50 20 次 DotBot 等
登录接口高频 15 2000 次 只作上下文,单独不足以定性(学生也会反复登录)
404 占比 25 比例 ≥50% 且 ≥20 次 目录/文件探测

分级:≥60 恶意 / ≥30 可疑 / 其余观察(观察级分数 <--min-observe-score(10) 不入库)。 安全约束:内网与白名单硬排除;单一接口高频的自动化客户端自动降级为 automation; 候选池取「所有规则命中的 IP ∪ 访问量 top」,因此低流量扫描器不会被漏掉。

地理信息(离线 GeoIP)

  • 列表新增「国家/地区」列(国旗 + 中文国名 + 省/州),详情页也显示
  • 数据来自 DB-IP Lite(免费、CC BY 4.0),已构建为本地 SQLite:data/geo/geo.db(144MB,360 万条 IPv4 段)
  • 检测时自动补全;已有 IP 在启动时自动回填
  • 更新地理库(每月):
cd data/geo
curl -O https://download.db-ip.com/free/dbip-city-lite-2026-10.csv.gz
gunzip -kf dbip-city-lite-2026-10.csv.gz
cd ../.. && python3 -m graylog_ops.geo \
  --city-csv data/geo/dbip-city-lite-2026-10.csv --out data/geo/geo.db
  • 关闭地理功能:--geo-db ""

新增 IP 推送通知

  • 只有 新增恶意 IP 或 观察/可疑 → 恶意升级 才会推送;同一 IP 不会重复推
  • 支持企业微信 / 钉钉 / 飞书 / Slack / 通用 JSON(type 字段选择)
  • 推送失败只打印日志,不影响检测循环
  • 配置:复制 notify.example.json 为 notify.json(已在 .gitignore 中),填 webhook:
{
  "type": "dingtalk",
  "url": "https://oapi.dingtalk.com/robot/send?access_token=你的TOKEN",
  "secret": "SEC你的加签密钥",
  "link_prefix": "http://179.253.234.124:12139/s/301b.../",
  "title": "新增恶意 IP"
}
type 说明
wecom 企业微信群机器人(markdown)
dingtalk 钉钉自定义机器人(markdown,支持 secret 加签)
feishu 飞书自定义机器人(text)
slack Slack Incoming Webhook
generic POST {"title","text"},可接自己的网关

测试与自检:

# 发一条测试消息(验证 webhook 是否通)
./bin/graylog-ops --config graylog-ops.json watch --notify-config notify.json --notify-test \
  --stream F5_Accesslog --profile profiles/f5.json --db data/ips.db

推送内容示例:

**新增恶意 IP**
**112.32.1.211**(中国 · 广东)
评分 100.0 · 请求 108 次
首次攻击 2026-10-17T18:24:09Z
命中:Spring/中间件探测 40 次;配置/凭证探测 8 次
[查看详情](http://179.253.234.124:12139/s/301b.../#/ip/112.32.1.211)

状态指示灯

页面右上角有一个圆点,表示后端与 Graylog 的连接状态:

  • 绿色:最近一轮检测成功
  • 红色:最近一轮检测失败
  • 黄色:状态过期
  • 灰色:尚未开始检测

不展示文字,悬停可看提示。

行为检测(已并入主服务,单进程)

特征检测和行为检测现在跑在同一个进程里:

  • 特征检测线程:每 60 秒一轮(原有逻辑)
  • 行为检测线程:每 15 分钟一轮(24h 窗口,纯行为评分,不依赖规则和名单)
  • 同一个 Web 前端:融合首页 + 融合详情

融合名单规则:

  • 只有「建议封禁」才展示和推送:特征命中攻击证据,或行为判定为建议封禁(非共享出口)
  • 共享出口/NAT、人工确认、数据不足等行为结果不展示、不推送
  • 一个 IP 只出现一行,来源标「特征 / 行为 / 双来源」
  • 首页按 first_banned_at(首次进入建议封禁的时间)倒序,每个 IP 只置顶一次,后续再命中不改变位置
  • 历史封禁 IP 永久保留在列表里(不参与 24h 再推)

推送:

  • 融合成一条消息,同一 IP 永远不重复推
  • 同一 IP 24 小时内最多推一次;24 小时后仍在活动(最近 24h 有流量)的 IP 会再推一条
  • 首次部署时历史 IP 静默(不补推)

详情页:同一 IP 融合展示「行为检测画像」和「特征检测证据」。

相关参数:--behavior-interval 900(行为刷新间隔)、--no-behavior(关闭行为检测)。

ASN / IP 归属

  • 列表「位置 / 归属」合并为一列、上下两行:第一行国家/地区,第二行图标 + 厂商/机构(如 ☁️ 阿里云、📱 中国移动)
  • 悬停显示 ASN 号、完整组织名和网络类型
  • 详情页显示 ASN 号 + 厂商 + 组织全名 + 类型
  • CSV 导出包含 asn, asn_org, asn_brand, asn_type
  • 品牌映射在 asn_brand_map.json(可继续加厂商),数据库 data/geo/asn.db
  • 更新方式(DB-IP ASN Lite,每月):
curl -O https://download.db-ip.com/free/dbip-asn-lite-2026-10.csv.gz
gunzip -kf dbip-asn-lite-2026-10.csv.gz
python3 -m graylog_ops.asn --csv data/geo/dbip-asn-lite-2026-10.csv --out data/geo/asn.db

关闭 ASN:--asn-db ""

名单语义(重要)

  • 永久保留:一旦某个 IP 被判定为恶意(ever_malicious=1 + last_malicious_at), 就永远不会被 --forget 清理,即使它后来降级为可疑/观察,也仍留在核心名单和导出名单里。 只有"从未判过恶意"的观察级 IP 才会按 --forget 过期删除。
  • 时间字段全部来自日志,不是检测时间:
    • 首次攻击 = 日志中最早命中攻击规则的时间(最多回溯 36 小时)
    • 最近攻击 = 日志中最近一次命中攻击规则的时间
    • 最近出现 = 该 IP 最后一条日志的时间
    • hits 只是"被检测到的轮次",不是攻击次数
  • 只有窗口内计数发生变化时才回查并更新时间,避免滚动窗口把"最近攻击"不断刷成当前时间
  • 历史数据用 watch --fix-times(别名 --fix-first-seen)修正
  • 默认排序 = 首次攻击倒序(最新攻击的在最上面),同一发现时间下最近还在攻击的排前面; 面板右上角可切换「最近攻击(最新优先)」「评分」「请求数」。
  • blocklist.txt 导出的是历史累计的全部恶意 IP,不是当前窗口快照。
  • 乱码处理:源日志里非 UTF-8 的字段会被清洗,识别为二进制时显示 [非 UTF-8 数据], URI/方法/状态码等字段不会再把乱码带进页面。

启动方式(按需,不挂后台服务)

按当前约定不安装 launchd/systemd,需要看的时候手动起(DSH/终端重启后进程会消失,重新执行即可):

cd "/Users/deepfal/Code/Work/ip sec/graylog-ops"
./bin/graylog-ops --config graylog-ops.json watch \
  --stream F5_Accesslog --profile profiles/f5.json \
  --interval 60 --window 3600 --forget 604800 \
  --db data/ips.db --host 0.0.0.0 --port 8088 \
  --allow allowlist.example.txt --export data/blocklist.txt --warm-limit 5

停止:Ctrl+C(或用 pkill -f "graylog_ops watch")。

维护命令:

# 用日志重新校准历史 IP 的首次攻击 / 最近攻击 / 最近出现时间(幂等)
./bin/graylog-ops --config graylog-ops.json watch --fix-times \
  --stream F5_Accesslog --profile profiles/f5.json --db data/ips.db

# 为名单里的恶意 IP 预生成证据快照
./bin/graylog-ops --config graylog-ops.json watch --warm-only --warm-limit 200 \
  --stream F5_Accesslog --profile profiles/f5.json --db data/ips.db

两日实测结果(本地 10-17 00:00 – 10-18 01:00)

窗口 89,600 秒(约 24.9 小时),12 条规则,--size 600:

55 个候选 → 48 个恶意 = 44 个高置信 + 4 个疑似共享出口;4 个可疑。

探测类型 典型路径 规则
Spring/中间件 /actuator*、/druid*、/nacos*、/swagger*、/jmx*、/heapdump*、/console*、/manager/html*、/solr* probe_java
配置/凭证 /.env*、/.git*、/.svn*、/.aws*、/.ssh*、/appsettings*、/WEB-INF*、/backup* probe_config
CMS /wp-*、/xmlrpc.php、/phpmyadmin*、/administrator*、/joomla*、/thinkphp* probe_cms
IoT/路由器 /ISAPI*、/LAPI*、/HNAP1*、/boaform*、/GponForm*、/cgi-bin/luci* probe_iot
DevOps/监控 /jenkins*、/grafana*、/kibana*、/zabbix*、/prometheus*、/consul*、/harbor* probe_devops
WebShell/上传 /shell.php、/cmd.php、/eval-stdin.php、/phpinfo*、/vendor/phpunit* probe_webshell
注入/穿越 etc/passwd、jndi:、union select、%2e%2e、%252f、base64_decode exploit_payload
登录撞库 /login*、/passport*、/captcha* + 401/403 login_attack / login_burst
扫描器/爬虫 sqlmap/nikto/nuclei/ffuf…、DotBot bad_ua / bot_ua

高置信名单(截至本轮回填,已写入 data/blocklist.txt):

  117.136.27.147    113.31.155.142    113.31.155.219    116.176.186.63  
  39.144.210.4      120.232.115.57    39.144.210.0      186.241.107.91  
  91.92.242.37      36.159.247.84     113.201.250.120   42.91.116.131   
  124.152.12.48     120.83.247.23     223.198.163.145   217.60.242.58   
  112.32.8.112      119.42.146.230    120.214.218.174   213.165.242.55  
  216.219.95.217    190.122.240.80    91.194.91.202     182.16.82.226   
  109.105.209.20    42.94.151.105     5.188.86.234      209.99.189.217  
  165.154.218.226   115.190.159.225   51.158.145.102    111.229.129.233 
  223.123.92.48     222.83.114.16     109.105.209.18    43.136.79.172   
  91.92.243.72      35.209.176.178    47.121.125.205    109.205.176.10  
  130.12.180.171    130.12.180.168  

疑似共享出口 / NAT(4 个,排除在名单外):42.92.144.29(653344)、27.185.218.213(125241)、118.181.187.248(30978)、42.94.148.133(22757)

判定公式:(请求量 >= 50000 且 探测占比 < 2%) 或 (请求量 >= 20000 且 探测占比 < 0.5%) → 标记 risk=shared_egress,导出名单时自动剔除。

规则收紧记录:/uploadfiles/*(正常文件目录)和 /database/*(正常业务路径)曾被 误判为 WebShell/配置探测,已从前缀里移除;/config.json、/settings.json 同理。

profile 格式

discover --save-profile 生成的骨架,judge --profile 读取:

{
  "fields": {
    "client_ip": "client_ip",
    "status": "status",
    "uri": "request_uri",
    "ua": "http_user_agent",
    "method": "request_method"
  },
  "signals": {
    "sensitive_path": {"query": "request_uri:wp-login", "weight": 30, "saturate": 3}
  },
  "thresholds": {
    "min_requests": 30,
    "ban_score": 70
  }
}

fields 可被命令行覆盖(--field / --status-field / --uri-field / --ua-field / --method-field), signals 里 enabled: false 可以关掉某个信号。

Graylog 4.0.8 接口笔记

用途 路径
版本/集群 GET /api/system
节点 GET /api/system/cluster/nodes
索引器健康 GET /api/system/indexer/cluster/health
字段类型 GET /api/system/fields
stream GET /api/streams
input GET /api/system/inputs
index set GET /api/system/indices/index_sets?skip=0&limit=100
索引 GET /api/system/indexer/indices
事件定义 GET /api/events/definitions?page=1&per_page=50
通知 GET /api/events/notifications
相对时间搜索 GET /api/search/universal/relative?query=*&range=300&limit=100&sort=timestamp:desc
绝对时间搜索 GET /api/search/universal/absolute?query=*&from=...&to=...
terms 聚合 GET /api/search/universal/terms?field=client_ip&range=300&size=50
直方图 GET /api/search/universal/histogram?query=*&range=3600&interval=1m
统计 GET /api/search/universal/stats?field=...&stats=cardinality,count
stream 过滤 追加 filter=streams:<streamId>

4.0 的关键点:

  • legacy 搜索 API 是主路径。/api/views/search(pivot)在 4.0 已经存在,但 DTO 与 5.x/6.x 差异大, 本工具不用它,避免被版本升级打断。需要时可用 raw 直接调。
  • size / limit 在 terms 端点上的参数名历史上不一致,客户端遇到 400 会自动换另一个再试,probe 会显示实际生效的那个。
  • 时间一律 UTC ISO8601,返回形如 2026-10-17T15:00:00.000Z。
  • 搜索返回是 {"messages": [{"message": {...}}, ...]},工具已摊平。
  • 4.0.8 跑在 Java 8 / CentOS 7 上,/api/system 不返回 ES 版本;后端是 ES 6.8 或 7.10。

4.0.8 实测踩坑(本环境已验证)

这几条是拿着真实 token 打 edu-master 得出的,不是文档抄的:

  1. terms / histogram / stats 在 4.0 已移除(返回 404)。 聚合必须走 views API:POST /api/views/search/sync(pivot)。 工具会自动探测:views 可用就用 views,3.x 上自动回退到 legacy 端点。 pivot 的行组是 {"type":"values","field":"uri","limit":50}(注意是 field 不是 fields)。
  2. 字段是 keyword 语义,查询有三条铁律:
    • uri:ketang → 0 条;uri:/ketang-studyapi/apis* → 46 万条(只有尾部通配符有效)
    • 未转义的 / 会被 Lucene 当成正则分隔符:uri:/manager/html* 会误命中所有含 html token 的文档(实测 9079 条,全是 UEditor 的 JS 静态文件)。必须写成 uri:\/manager\/html*,或引号精确匹配 uri:"/wp-login.php"
    • 前导通配符 uri:*admin* 被禁用(直接 500/400) judge 的默认信号查询已按此修正(ua:sqlmap*、uri:\/actuator*)。
  3. 部分 F5 VS 的 HTTP 字段为空串:1 小时样本里 uri 约 98% 有值、status 约 48%、 ua 约 48%。四层 VS 只有 client_ip/server_ip/method 等基础字段。
  4. status 是混格式字符串(200 OK / 404 / 502 Bad Gateway),status:404 仍能按 token 命中;status_code 字段全空,不要用。
  5. event_type 只有 req,不存在 req/resp 双份,不需要去重。

本环境实测结论(2026-10-18)

项 结论
集群 4 节点:edu-master(master) + edu-slave1/3/4,2354 个 active shard,deflector graylog_457
数据链路 F5(10.180.241.2/3) --syslog--> Graylog。深信服 AD 不在 F5 访问日志链路上(它对应的是 DNS 类业务)
活跃 stream F5_Accesslog(约 11 万条/5 分钟)、F5 Attack Logs、有源业务数据-Nat-Stream(NAT 会话)、syslog_vpn_stream(VPN);站群、全量站点数据 24h 内无数据
量级 F5_Accesslog 约 130 万条/小时(≈360 条/秒)
字段 client_ip / server_ip / uri / host / ua / status / method / bigip / vs;没有 XFF 字段
关键事实 10.185.9.41(46%) 和 10.185.9.15(45%) 两个内网 IP 占 91% 流量,是内部 API 轮询(/ketang-studyapi/apis/*,一半是 404),必须白名单/内网排除;外网用户 F5 直见真实 IP,所以 client_ip 可用于封禁

真实判定样例(1 小时窗口,--verify 5):

100.0  ban_1h   186.241.107.91   1164   认证失败 24; 敏感路径 36; 404 探测 156
 68.5  ban_30m  113.201.250.120   612   认证失败 3; 敏感路径 6; 404 探测 5
 54.0  observe  124.70.2.111      666   认证失败 333(集中在单接口,已降级)
  • 186.241.107.91 挖到 /actuator/env、/actuator/xyzabc(fuzz)、/actuator;/health(分号绕过尝试)
  • 113.201.250.120 同样在打 /actuator/env
  • 124.70.2.111 每 10.6 秒 POST 同一个业务 API、稳定 403 —— 自动化客户端 token 过期, 被 --verify 识别并降级为 observe,不误封

--verify N 会对前 N 个 ban 候选多做一次 URI 聚合:若流量 90% 以上集中在单一非登录接口, 判定为"疑似业务客户端/自动化任务"并降级。这是本环境实测后加的必要规则。

权限

只读账号给这些权限就够(缺哪个会报 403,probe 会逐条标出来):

messages:read            searches:relative        searches:absolute
searches:keyword         streams:read             inputs:read
indexsets:read           eventdefinitions:read    eventnotifications:read

首次摸底建议直接用管理员 token,确认链路通了再收窄到只读 token。

已知限制

  • 纯只读,不提供写入/告警创建(下一阶段再做,会单独走 --apply 路径)。
  • judge 是单字段聚合:只按 client_ip 分组,不做多字段联合(如 IP×URI)。 这是刻意的取舍——固定查询次数,代价是拿不到"同一 IP 命中几种不同敏感路径"这类信号。 需要时用 --explain 抽样人工看。
  • 每类聚合最多看 top --size(默认 200)个 IP;超出部分不参与判分。
  • discover 用 offset 分页采样,适合几千条量级;十万级请改用 terms / histogram。
  • 大时间窗 + 高基数 terms 会打 ES,--range 别一上来就 7d;先 histogram 看量。

与执行侧衔接(下一步)

判断层已经产出 {ip, action, score, reasons, signals}。执行侧有两个候选:

  1. Graylog 事件 + HTTP 通知:事件定义按 client_ip 分组 count 超阈值 → 通知到 ban-service。 优点是近实时;缺点是阈值表达力有限,复杂判定还得在服务里做。
  2. 定时跑 judge(如每 1 分钟)+ 直接调 AD 接口:
cron: graylog-ops judge --range 120 --json --out /var/lib/ban/candidates.json
      → ban-service 读 candidates
      → 白名单/冷却/幂等校验
      → 调 AD 黑名单 API 下发
      → TTL 到期自动删除 / 复核

AD 接口这块先不猜。截图里管理入口是 https://10.230.13.25:4433/index?x_id=<...>#mod=/mod_runtime/homepage/index, x_id 看着像会话/鉴权参数,前端是 SPA 路由,真正的 XHR 端点得抓包确认。需要你做的只有一件事:

在能访问 AD 的机器上打开 Chrome DevTools → Network → 勾选 Fetch/XHR → 手工做一遍"添加/删除一条 IP 黑名单" → 右键 Copy as cURL(或导出 HAR)发我。

需要覆盖的请求:登录、会话刷新、业务/节点列表、黑名单增删查、下发后的生效确认。 拿到之后我补一个 sangfor 模块(登录态维持、黑名单 CRUD、幂等、限流),复用现有的 HTTP 重试/错误归一。

环境备忘

项 值
Graylog 4.0.8+6b8c55d @ edu-master:9002(4 节点,Java 8 / CentOS 7)
深信服 AD 7.0.8R5,UI https://10.230.13.25:4433,HA(主)10.1.1.1,业务 5 / 节点 3
网络 开发机可访问 10.180.254.17:9002 与 10.230.13.25:4433
凭据 graylog-ops.json(chmod 600,已 gitignore),只读 token

开发

python3 tests/demo.py                      # 用假服务预览各命令输出,不需要真 Graylog
python3 -m unittest discover -s tests      # 41 项单测 + 端到端测试

测试不需要真实 Graylog:tests/fake_graylog.py 起一个本地 HTTP 服务,按 4.0 的响应结构回包, 并实现了一个很小的 Lucene 子集(field:value、field:(a OR b))来验证 judge 的查询构造。 要加新接口,先在这个假服务里加路由,再补一条 e2e。

graylog_ops/
  http.py       urllib 封装:Basic auth、X-Requested-By、重试、错误归一
  config.py     CLI / 环境变量 / JSON 文件三级配置
  client.py     4.0 REST 接口 -> 普通 dict/list,容忍响应形状差异
  detect.py     客户端 IP 字段推断(IP 解析、内部地址判定、评分)
  judge.py      判断层:信号采集、基线、评分、动作映射、白名单
  commands.py   各子命令(构建 JSON + 人读渲染)
  render.py     CJK 宽度对齐的表格
tests/
  fake_graylog.py  假 Graylog 4.0 服务(含查询子集)
  test_detect.py   字段推断单测
  test_judge.py    判断层测试(暴力破解/扫描/正常大流量/内网/白名单)
  test_e2e.py      CLI 端到端

排错

现象 原因 / 处理
连接超时 服务在 9002 端口(9000 是空等)。9002 也不通时走 VPN / 内网跳板机
401 token 错或已吊销;确认是"API token"不是 session token
403 账号缺权限,看 probe 里哪项 FAIL
404 该版本没这个接口,用 raw /api/... 验证,或 probe 看矩阵
400 / 422 参数不被接受,加 -v 看实际请求 URL
discover 没结果 时间窗内无数据 / stream 过滤错 / 查询语法错 / 无权限(静默返回空)
discover 推荐的还是代理 IP 采集端没解析 XFF;需要 AD 透传 + nginx log_format 落真实 IP
judge 没候选 窗口内 IP 数低于 --min-requests,或分数低于 --min-score;先 --all 看全量
judge 信号命中 0 查询模板与字段分词不匹配(见"判断规则"末尾),用 profile 覆盖 query
封禁候选里出现自己的设备 白名单没写全;把 AD 地址/VIP/回源/监控补进 --allow 文件