初始提交:恶意 IP 特征+行为融合检测系统
- 特征检测(规则/签名)与行为检测(24h 统计)单进程运行 - 融合名单按首次发现时间倒序,展示位置/归属(GeoIP + ASN) - 钉钉融合推送(加签、24h 节流、去重) - 页面状态灯、CSV 导出、Docker 镜像构建文件
This commit is contained in:
commit
2d76ed6672
42 files changed
+10645
No files matched your search
@@ -0,0 +1,672 @@
|
||||
# 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+ 标准库。
|
||||
|
||||
```bash
|
||||
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 请求)。
|
||||
|
||||
## 五步摸底
|
||||
|
||||
```bash
|
||||
# 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)
|
||||
|
||||
```bash
|
||||
# 持续运行(面板 + 检测循环)
|
||||
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 秒自动刷新(可关闭)
|
||||
|
||||
界面截图:[列表页](docs/screenshots/list.png) · [详情页](docs/screenshots/detail.png)
|
||||
- **一键导出**(右上角两个下拉,都作用于当前筛选/排序结果):
|
||||
- 「复制 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 在启动时自动回填
|
||||
- 更新地理库(每月):
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```json
|
||||
{
|
||||
"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"}`,可接自己的网关 |
|
||||
|
||||
测试与自检:
|
||||
|
||||
```bash
|
||||
# 发一条测试消息(验证 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,每月):
|
||||
|
||||
```bash
|
||||
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/终端重启后进程会消失,重新执行即可):
|
||||
|
||||
```bash
|
||||
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"`)。
|
||||
|
||||
维护命令:
|
||||
|
||||
```bash
|
||||
# 用日志重新校准历史 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` 读取:
|
||||
|
||||
```json
|
||||
{
|
||||
"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 |
|
||||
|
||||
## 开发
|
||||
|
||||
```bash
|
||||
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` 文件 |
|
||||
Reference in new issue
Block a user