Files

736 lines
28 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# graylog-ops
基于 **Graylog + F5 访问日志** 的恶意 IP 检测与封禁清单服务。
它做三件事:
1. **特征检测**:用 12 条规则识别扫描器、漏洞探测、登录爆破、恶意 UA 等攻击行为;
2. **行为检测**:独立于规则和名单,按 24 小时访问行为给 IP 打分;
3. **融合输出**:只保留"建议封禁"的 IP,形成一个去重、可分页、可推送的清单,并提供监控面板。
服务是**单进程**的:检测线程 + 行为线程 + Web 面板跑在同一个进程里,数据落在本地 SQLite。
![列表页](docs/screenshots/list-pagination.png)
![监控页](docs/screenshots/monitor.png)
---
## 功能特性
- **特征 + 行为双检测**,融合后按 `first_banned_at` 倒序展示,同一 IP 只出现一次
- **只展示建议封禁**:共享出口(移动/教育网/CGNAT)、疑似多用户、归属不明的 IP 不进入清单、不推送
- **归属清晰**:国家/地区 + ASN(厂商/运营商,如阿里云、腾讯云、中国移动、教育网)+ 网络类型
- **分页 + 服务端搜索**:默认 20 条/页,支持 20/50/100,按 IP、归属、理由搜索
- **详情可复核**:行为画像、规则命中、请求频率/状态码/方法/域名分布、按需从 Graylog 拉取证据
- **融合推送**:一个 IP 24 小时内最多一条,仍活动可 24h 后再提醒;失败自动退避重试并记录审计
- **自动补测**:启动时发现检测缺口(默认 >30 分钟)自动后台回扫,补录期间漏掉的 IP
- **监控**:页面右上角状态灯 + 「监控」页面 + Prometheus `/metrics`
- **Docker 交付**:镜像内可带数据快照,首次启动自动初始化 `/data`
- **零第三方依赖**:纯 Python 标准库(geo/asn 查询用 SQLite)
---
## 架构与数据流
```
外部客户端
│
▼
F5(访问日志) ──syslog──► Graylog 4.0.x
│ API(只读)
▼
┌──────────────────────────┐
│ graylog-ops(单进程) │
│ │
│ 特征检测线程(60s) │
│ 行为检测线程(15min) │
│ 自动补测(启动时) │
│ Web 面板 / API │
└────────────┬─────────────┘
│ SQLite(data/ips.db)
▼
融合名单(建议封禁)
│
├── 钉钉/企微/飞书/Slack 推送
├── blocklist.txt 导出
└── /metrics 监控
```
Graylog 是旁路日志系统,本服务**只读**:不建流、不改索引、不下发封禁。封禁执行由 F5 / AD / 防火墙侧完成。
---
## 快速开始
### 环境要求
- Python **3.9+**(Docker 镜像使用 Python 3.12)
- 能访问 Graylog API(默认端口 **9002**)
- 可选:`data/geo/geo.db`、`data/geo/asn.db`(不提供也能跑,只是没有归属信息)
### 配置 Graylog
复制配置模板并填入 Graylog API token:
```bash
cp config.example.json graylog-ops.json
chmod 600 graylog-ops.json
# 编辑 graylog-ops.json:url / token
```
也可以用环境变量(优先级:命令行 > 环境变量 > 配置文件):
```bash
export GRAYLOG_URL=http://graylog.example:9002
export GRAYLOG_TOKEN='你的 API Token'
```
Token 认证方式是 **token 当用户名、字面量 `token` 当密码**,工具内部已处理,不需要额外配置。
> 只读权限即可:`messages:read`、`searches:*`、`streams:read` 等,详见文末「权限」。
### 启动服务
```bash
./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 \
--geo-db data/geo/geo.db \
--asn-db data/geo/asn.db \
--allow allowlist.example.txt \
--export data/blocklist.txt
```
打开:
```
http://127.0.0.1:8088/
```
如果不加 `--path-prefix`,根路径就是面板;用于公网分享时建议加前缀(见「安全」)。
### 可选:桌面/服务器常驻
按当前约定不写 launchd/systemd,需要时手动起;Docker 部署可加 `--restart unless-stopped`。
---
## 检测机制
### 1. 特征检测
每 `--interval` 秒(默认 60)跑一轮,所有规则一次性聚合,查询次数与 IP 数量无关。
| 规则 | 标题 | 权重 | 饱和点 | 说明 |
| --- | --- | ---: | ---: | --- |
| `probe_java` | Spring/中间件探测 | 60 | 2 | `/actuator`、`/druid`、`/nacos`、`/swagger`、`/heapdump`、`/weblogic` 等 |
| `probe_config` | 配置/凭证探测 | 60 | 2 | `/.env`、`/.git`、`/.ssh`、`/backup`、`/WEB-INF` 等 |
| `probe_cms` | CMS 探测 | 60 | 2 | `/wp-*`、`/phpmyadmin`、`/administrator`、`/thinkphp` 等 |
| `probe_iot` | IoT/路由器探测 | 60 | 2 | `/ISAPI`、`/HNAP1`、`/boaform`、`/cgi-bin/luci` 等 |
| `probe_devops` | DevOps/监控探测 | 60 | 2 | `/jenkins`、`/grafana`、`/kibana`、`/zabbix`、`/harbor` 等 |
| `probe_webshell` | WebShell/上传探测 | 60 | 2 | `/shell.php`、`/cmd.php`、`/phpinfo`、`/vendor/phpunit` 等 |
| `exploit_payload` | 注入/穿越载荷 | 65 | 2 | `etc/passwd`、`jndi:`、`union select`、`%2e%2e`、`base64_decode` 等 |
| `login_attack` | 登录失败尝试 | 55 | 8 | 登录类 URI 上的 401/403 |
| `login_burst` | 登录接口高频 | 15 | 2000 | 只作上下文,单独不足以定性 |
| `bad_ua` | 扫描器 UA | 55 | 1 | sqlmap / nikto / nuclei / nmap / zgrab 等 |
| `bot_ua` | 爬虫 UA | 50 | 20 | DotBot 等(按 UA 判断) |
| `not_found` | 404 探测 | 0 | — | 不直接计分,按 404 占比计分(≥50% 且 ≥20 次时最多加 25) |
评分分级:
| 分级 | 分数 | 是否进入融合名单 |
| --- | ---: | --- |
| `malicious` 恶意 | ≥ 60 | 是 |
| `suspicious` 可疑 | ≥ 30 | 否 |
| `automation` 自动化 | — | 单一接口高频的自动化客户端,降级处理 |
| `observe` 观察 | ≥ 10(`--min-observe-score`) | 否 |
安全约束:
- 内网地址(RFC1918 / 回环 / 链路本地 / CGNAT)默认排除
- `--allow` 白名单文件(IP/CIDR,一行一个,# 注释)硬排除
- 纯"请求量大"不会判恶意,必须有攻击证据或行为异常
### 2. 行为检测
每 `--behavior-interval` 秒(默认 900)跑一次,分析 **最近 24 小时** 的访问行为,**不依赖规则命中,也不依赖已有名单**。
评分项(>= 60 且至少 2 个信号才算行为违规):
| 信号 | 加减分 |
| --- | --- |
| 4xx 失败率 ≥50% / ≥30% | +30 / +15 |
| 不同 URI ≥20 / ≥8 | +20 / +8 |
| 短时爆发(覆盖 ≤30% 且峰值 ≥25%) | +20 |
| 登录接口占比 ≥20% | +15 |
| 访问多个 Host(≥2) | +5 |
| 单一 UA | +5 |
| 自动化 UA(SDK/伪造浏览器/其他) | +10 |
| 4xx 数量 ≥100 且失败率 ≥20% | +10 |
| 非 GET 为主(<30%) | −25 |
| 低失败率且 2xx ≥80% | −20 |
| 长期稳定单接口 | −20 |
| 状态字段缺失 | −10 |
行为违规后再结合 **ASN 出口属性** 给结论:
| 出口属性 | 结论 | 是否进融合名单 |
| --- | --- | --- |
| 移动网络 / 教育网(共享出口) | 仅告警(共享出口) | 否 |
| 宽带 + 多 UA/多 Host(疑似多用户) | 仅告警(疑似多用户) | 否 |
| 宽带 + 单 UA 且行为分 ≥75 | 建议封禁 | 是 |
| 宽带 + 单 UA 且行为分 <75 | 建议人工确认 | 否 |
| 云主机 / IDC 机房 | 建议封禁 | 是 |
| 归属不明 | 建议人工确认 | 否 |
行为检测的候选范围:24 小时访问量前列 IP + 额外 4xx 较多的 IP,最多约 600 条进入画像;这是为了控制 Graylog 查询量,必要时可调整代码中的上限。
### 3. 融合名单
```
fused = 特征 ever_malicious=1 OR 行为 verdict = 建议封禁
```
- **来源**:`feature`(特征)、`behavior`(行为)、`both`(双来源)
- **首次发现时间 `first_banned_at` 固定**:一个 IP 只会在进入名单时排一次序,后续再命中不会把它重新顶到最上面
- **历史永久保留**:一旦进入过融合名单,即使后来不再活跃也不会被删除
- **只有建议封禁会展示/推送**:其他行为结论只留在画像表里供详情页查看
### 4. 推送
所有推送走**同一条融合链路**,同一 IP 不会因为多个来源收到多条消息。
推送条件:
- 该 IP 最近 24 小时内仍有活动(`last_activity_at`)
- 距离上次推送 ≥ 24 小时(`last_pushed_at`)
- 不在失败退避期内(`push_next_at`)
消息内容:
```
**建议封禁 · 融合检测**
**113.194.199.54**(中国 · 江西)
归属:🏠 中国联通 · AS4837
来源:特征检测 · 发现:10-18 04:29
评分 100 · 请求 42 次
⏰ 该 IP 仍在活动(第 2 次提醒)
依据:特征命中:漏洞利用 payload 8 次、Spring/中间件探测 4 次、404 探测 2 次
[查看详情](http://<host>:<port>/s/<secret>/#/ip/113.194.199.54)
```
- 首次部署/首次同步时,历史 IP 会被标记为已推送,不会补推
- 推送失败:写入 `notifications` 表,按 **60s → 120s → 240s → … → 1h** 指数退避重试
- 连续失败 3 次会记录告警;可选 `NOTIFY_ALERT_URL` 发到独立 webhook
- 详情链接前缀由 `link_prefix`(或 `NOTIFY_LINK_PREFIX`)控制
本地配置:复制 `notify.example.json` 为 `notify.json`(`watch` 默认读取该文件,存在才启用),填入 webhook / 加签密钥 / 链接前缀;Docker 部署改用 `NOTIFY_*` 环境变量。示例:
```json
{
"type": "dingtalk",
"url": "https://oapi.dingtalk.com/robot/send?access_token=<token>",
"secret": "SEC<加签密钥>",
"link_prefix": "http://<host>:<port>/s/<secret>/",
"title": "新增恶意 IP"
}
```
不配置推送也能正常检测;需要验证 webhook 时用:
```bash
./bin/graylog-ops --config graylog-ops.json watch --notify-test --stream F5_Accesslog --profile profiles/f5.json --db data/ips.db
```
### 5. 自动补测
每轮检测成功后会写入 `meta.detect_last_success`。服务启动时:
- 缺口 ≤ `--backfill-threshold`(默认 1800 秒):正常启动,不补
- 缺口 > 阈值且 ≤ `--backfill-max-gap`(默认 7 天):**后台线程自动回扫**,不阻塞页面
- 缺口 > 7 天:跳过并打印日志
自动补测会:
1. 按规则回扫缺口时间段(自动做时间切片,避免聚合条数上限漏 IP)
2. 跑一次该时间段的行为分析(仅缺口 1–48 小时时)
3. 补录的 IP 全部抑制推送,不会补发钉钉
参数:
```bash
--backfill-threshold 1800 # 缺口阈值(秒)
--backfill-max-gap 604800 # 最大允许补测的缺口(秒)
--no-auto-backfill # 关闭自动补测
```
---
## Web 界面
### 列表页
- 列:IP 地址 / 位置 · 归属 / 来源 / 评分 / 请求数 / 频率 / 发现时间 / 最近活动 / 判定理由
- **位置 / 归属** 两行展示:第一行国旗 + 国家·省,第二行图标 + 厂商/运营商(悬停看 ASN 全名)
- **分页**:默认 20 条/页,可切 20 / 50 / 100;页码、上一页/下一页、总数
- **搜索**:服务端搜索,支持 IP、国家/地区、厂商、ASN、来源、判定理由;250ms 防抖
- 点击 IP 复制;点击行进入详情
- 右上角状态灯:绿=正常、红=检测失败、黄=状态过期、灰=未知(悬停看提示)
- 每 30 秒自动刷新(可关闭)
### 详情页
- 顶部:评分环、发现时间、最近活动、建议
- **行为检测画像**:结论、人话理由、出口(厂商 + ASN + 类型)、24h 请求/失败率/URI/UA 数、信号列表
- **判定依据**:命中的规则和次数
- **证据**:请求频率分布、状态码分布、请求方法分布、访问域名分布、请求样本;证据按需从 Graylog 拉取并缓存,索引轮转后回退到本地快照
![详情页](docs/screenshots/detail-asn.png)
### 监控页
入口:页面右上角「监控」,或 `#/monitor`。
展示:
- Graylog 连接状态、最近成功、距上次成功、最近错误
- 特征检测轮次、失败次数、最近耗时
- 行为分析次数、最近耗时、最近违规数
- 推送 24h 成功率、成功/失败数、连续失败、最近错误
- 名单总数、今日新增、双来源、行为新发现
- 自动补测次数、上次时间/区间/新增
- 最近推送记录(成功/失败 + 错误)
### 复制 / 导出
- 「复制 IP」:逗号分隔 / 换行分隔
- 「导出」:CSV(完整字段) / 仅 IP 列表
- 都作用于**当前搜索条件下的全部记录**,不是只导出当前页
---
## HTTP API
服务启动后(假设无路径前缀):
| 接口 | 说明 |
| --- | --- |
| `GET /healthz` | 健康检查,返回 `{"ok": true}`(不受路径前缀限制) |
| `GET /api/status` | Graylog 连通状态、最近成功时间、错误 |
| `GET /api/stats` | 名单/分级的聚合统计 |
| `GET /api/ips` | 融合名单分页接口 |
| `GET /api/ip/<ip>` | 单 IP 详情(含行为画像、融合记录、证据) |
| `GET /api/export.csv` | 特征库导出(CSV,legacy,按 `scope=core/all`) |
| `GET /api/export.txt` | 特征库导出(纯 IP,每行一个) |
| `GET /metrics` | Prometheus 指标 |
| `GET /api/monitor` | 监控页数据(JSON) |
### `/api/ips` 参数
| 参数 | 说明 |
| --- | --- |
| `page` | 页码,默认 1 |
| `page_size` | 每页条数,默认 50,最大 2000(兼容旧参数 `limit`) |
| `offset` | 自定义偏移,优先于 page |
| `q` | 搜索关键词(IP/归属/ASN/来源/理由) |
返回:
```json
{
"count": 20,
"total": 106,
"page": 1,
"page_size": 20,
"pages": 6,
"q": "",
"updated_at": "2026-10-19T05:25:03Z",
"items": [
{
"ip": "121.41.167.124",
"sources": "feature",
"tier": "malicious",
"score": 60.0,
"feature_score": 60.0,
"behavior_score": null,
"total": 6,
"rules": {"probe_iot": 6},
"reasons": ["特征命中:IoT/路由器探测 6 次"],
"first_seen": "2026-10-19T02:14:07.000Z",
"last_seen": "2026-10-19T02:18:56.000Z",
"window": 3600,
"country_name": "中国",
"region": "浙江",
"asn": "37963",
"asn_brand": "阿里云",
"asn_type": "cloud",
"asn_icon": "☁️"
}
]
}
```
### `/metrics` 指标
```
ipsec_detect_rounds_total
ipsec_detect_errors_total
ipsec_detect_last_success_timestamp
ipsec_detect_gap_seconds
ipsec_detect_last_duration_seconds
ipsec_behavior_runs_total
ipsec_behavior_errors_total
ipsec_behavior_last_duration_seconds
ipsec_behavior_last_violations
ipsec_fused_total
ipsec_fused_new_24h
ipsec_fused_both
ipsec_push_sent_total
ipsec_push_failed_total
ipsec_push_consecutive_failures
ipsec_push_success_ratio_24h
ipsec_graylog_up
```
---
## 数据模型
SQLite 文件默认 `data/ips.db`,开启 WAL。主要表:
| 表 | 作用 |
| --- | --- |
| `ip_stats` | 特征检测结果:IP、分级、评分、规则命中、原因、首次/最近时间、Geo/ASN、`ever_malicious` |
| `behavior_profiles` | 行为画像:评分、结论、信号、理由、ASN/网络类型、24h 指标、`historical`(补测标记) |
| `ip_fused` | 融合名单:来源、首次封禁时间、最近活动、推送时间/次数、失败退避、融合理由 |
| `meta` | 运行状态:最近成功检测、各计数器、自动补测记录 |
| `notifications` | 推送审计:IP、通道、成功/失败、尝试次数、错误、时间 |
| `events` | 检测事件历史 |
时间字段统一存 **UTC ISO8601**(如 `2026-10-19T05:41:59Z`),页面和推送按浏览器/服务器本地时区展示。
---
## 命令行
### 主命令
| 命令 | 说明 |
| --- | --- |
| `probe` | 连通性/认证/接口可用性矩阵 |
| `system` | 版本、节点、索引器健康、license |
| `streams` | stream 列表 |
| `inputs` / `fields` / `indices` / `alerts` | 环境信息 |
| `discover` | 采样日志,推断真实客户端 IP 字段 |
| `judge` | 只读判断:聚合信号 → 封禁候选 |
| `top-ips` / `terms` / `messages` / `histogram` | 查询/聚合 |
| `snapshot` | 环境快照落盘 |
| `raw` | 直接调用任意 API |
| `collect` | 一条命令产出 probe/discover/judge/snapshot |
| `watch` | 持续检测 + Web 面板(本服务主入口) |
通用参数:`--json`、`--out FILE`、`-v`。
### watch 常用参数
| 参数 | 默认 | 说明 |
| --- | --- | --- |
| `--stream` | — | Graylog stream 名称或 ID |
| `--profile` | — | 字段映射(`profiles/f5.json`) |
| `--interval` | 60 | 特征检测间隔(秒) |
| `--window` | 900 | 特征检测窗口(秒);F5 场景建议 3600 |
| `--forget` | 7200 | 多久未出现从观察列表移除(秒);恶意 IP 永久保留 |
| `--db` | — | SQLite 路径 |
| `--host` / `--port` | 127.0.0.1 / 8088 | 面板监听 |
| `--path-prefix` | — | 对外访问密钥路径(根路径 404) |
| `--allow` | — | 白名单文件,可重复 |
| `--export` | — | 每轮导出恶意 IP 到纯文本 |
| `--behavior-interval` | 900 | 行为分析间隔(秒) |
| `--no-behavior` | — | 关闭行为检测 |
| `--geo-db` / `--asn-db` | data/geo/*.db | 归属库;空字符串禁用 |
| `--backfill-threshold` | 1800 | 启动自动补测阈值(秒) |
| `--backfill-max-gap` | 604800 | 最大补测缺口(秒) |
| `--no-auto-backfill` | — | 关闭自动补测 |
| `--notify-config` | — | webhook 配置(`notify.json`) |
| `--notify-test` | — | 发一条测试推送后退出 |
| `--once` | — | 只跑一轮后退出 |
| `--serve-only` | — | 只提供面板,不检测 |
| `--fix-times` | — | 用日志修正历史时间字段 |
| `--warm-only` | — | 预生成证据快照后退出 |
| `--min-observe-score` | 10 | 低于该分数的观察级不入库 |
| `--size` | 200 | 每类聚合返回 IP 上限 |
| `--verify` | 8 | 对前 N 个候选做 URI 核验 |
### 维护/补测工具
```bash
# 特征检测补测(支持自动时间切片)
python3 tools/backfill_gap.py \
--from "2026-10-18 04:00" --to "2026-10-19 03:55" [--dry-run]
# 行为检测补测(历史画像会标记 historical=1,不会被实时分析覆盖)
python3 tools/backfill_behavior.py \
--from "2026-10-18 04:00" --to "2026-10-19 03:55" [--dry-run]
```
时间参数使用**本地时间**;补测结果默认抑制推送。
---
## Docker 部署
### 镜像结构
`docker-build/` 是构建上下文:
```
docker-build/
├── Dockerfile
├── entrypoint.sh
├── app/ # 代码(构建时生成/复制,gitignore)
│ ├── graylog_ops/
│ ├── bin/
│ ├── profiles/
│ ├── allowlist.example.txt
│ └── asn_brand_map.json
└── seed/ # 数据快照(构建时生成/复制,gitignore)
├── ips.db
├── blocklist.txt
└── geo/
├── geo.db
└── asn.db
```
启动时如果 `/data/ips.db` 不存在,entrypoint 会把 `/app/seed/` 复制到 `/data`;已有数据卷则继续使用卷内数据。
### 构建
```bash
# 1) 准备 app/ 和 seed/(示例)
mkdir -p docker-build/seed/geo
rsync -a --exclude '__pycache__' graylog_ops docker-build/app/
rsync -a bin profiles docker-build/app/
cp allowlist.example.txt asn_brand_map.json docker-build/app/
# 2) 复制当前数据库快照(推荐用 sqlite backup 保证一致)
python3 - <<'PY'
import sqlite3
src = sqlite3.connect("data/ips.db")
dst = sqlite3.connect("docker-build/seed/ips.db")
with dst:
src.backup(dst)
src.close(); dst.close()
PY
cp data/geo/geo.db data/geo/asn.db docker-build/seed/geo/
cp data/blocklist.txt docker-build/seed/
# 3) 构建并导出离线包
docker build -t malicious-ip-watch:YYYY-MM-DD docker-build
docker save malicious-ip-watch:YYYY-MM-DD | gzip > malicious-ip-watch-YYYY-MM-DD.tar.gz
```
### 运行时环境变量
| 变量 | 默认 | 说明 |
| --- | --- | --- |
| `GRAYLOG_URL` | — | Graylog 地址(必填) |
| `GRAYLOG_TOKEN` | — | API token(必填) |
| `GRAYLOG_STREAM` | `F5_Accesslog` | stream 名称 |
| `APP_PORT` | 8088 | 容器内监听端口 |
| `APP_PATH_PREFIX` | — | 对外访问路径前缀(建议设置) |
| `CHECK_INTERVAL` | 60 | 检测间隔 |
| `WINDOW` | 3600 | 检测窗口 |
| `FORGET` | 604800 | 观察列表过期时间 |
| `BEHAVIOR_INTERVAL` | 900 | 行为分析间隔 |
| `NO_BEHAVIOR` | 0 | 设为 1 关闭行为检测 |
| `WARM_LIMIT` | 5 | 每轮证据快照数量 |
| `DATA_DIR` | `/data` | 数据目录 |
| `NOTIFY_TYPE` | generic | `dingtalk`/`wecom`/`feishu`/`slack`/`generic` |
| `NOTIFY_URL` | — | webhook 地址 |
| `NOTIFY_SECRET` | — | 钉钉加签密钥(`SEC...`) |
| `NOTIFY_LINK_PREFIX` | — | 详情链接前缀 |
| `NOTIFY_TITLE` | 新增恶意 IP | 推送标题 |
| `NOTIFY_ALERT_URL` | — | 推送连续失败的独立告警 webhook(可选) |
| `NOTIFY_ALERT_TYPE` / `NOTIFY_ALERT_SECRET` | — | 告警通道类型/加签密钥 |
镜像内**不含任何凭证**,`graylog-ops.json`、`notify.json` 不会打进镜像。
### 运行
```bash
docker load -i malicious-ip-watch-YYYY-MM-DD.tar.gz
docker run -d --name malicious-ip-watch --restart unless-stopped \
-p 8088:8088 \
-v malicious-ip-data:/data \
-e GRAYLOG_URL=http://10.180.254.17:9002 \
-e GRAYLOG_TOKEN='你的 token' \
-e GRAYLOG_STREAM=F5_Accesslog \
-e APP_PORT=8088 \
-e APP_PATH_PREFIX='/s/<随机密钥>' \
-e BEHAVIOR_INTERVAL=900 \
-e NOTIFY_TYPE=dingtalk \
-e NOTIFY_URL='https://oapi.dingtalk.com/robot/send?access_token=...' \
-e NOTIFY_SECRET='SEC...' \
-e NOTIFY_LINK_PREFIX='http://<公网地址>/s/<随机密钥>/' \
malicious-ip-watch:YYYY-MM-DD
```
- 想使用镜像内数据快照:使用**新的数据卷名**,或先 `docker volume rm` 旧卷
- 想保留线上数据:继续使用原数据卷,镜像里的 seed 不会覆盖
- 健康检查:`/healthz`,容器状态应为 `healthy`
---
## 归属数据(GeoIP / ASN)
### GeoIP
- 数据库:`data/geo/geo.db`(DB-IP Lite City,约 144MB,360 万 IPv4 段)
- 列表显示中文国家名 + 省/州;详情显示城市
- 更新(每月):
```bash
curl -O https://download.db-ip.com/free/dbip-city-lite-2026-10.csv.gz
gunzip -kf dbip-city-lite-2026-10.csv.gz
python3 -m graylog_ops.geo \
--city-csv data/geo/dbip-city-lite-2026-10.csv \
--out data/geo/geo.db
```
关闭:`--geo-db ""`
### ASN / 厂商
- 数据库:`data/geo/asn.db`(DB-IP ASN Lite,约 18MB)
- 品牌映射:`asn_brand_map.json`(38+ 条关键词 → 厂商/运营商 + 图标 + 类型)
- 更新:
```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_brand_map.json` 的 `items` 里加关键词、品牌名、类型、图标即可,不用改代码。
类型取值:`cloud`(云服务商)、`idc`(IDC/机房)、`mobile`(移动网络)、`broadband`(宽带)、`education`(教育网)、`enterprise`(企业/政府)、`unknown`(未知)。
关闭:`--asn-db ""`
---
## 目录结构
```
graylog-ops/
├── bin/graylog-ops # 启动脚本
├── graylog_ops/
│ ├── cli.py # 参数解析/子命令注册
│ ├── commands.py # 各子命令实现
│ ├── watch.py # 主服务:检测、存储、Web、API、监控
│ ├── behavior.py # 行为检测 + 融合推送
│ ├── backfill.py # 自动补测/历史回扫
│ ├── notify.py # 推送通道(钉钉加签等)
│ ├── geo.py # GeoIP 构建/查询
│ ├── asn.py # ASN 构建/查询/品牌映射
│ ├── client.py / http.py # Graylog API 客户端
│ ├── detect.py / judge.py # 字段推断/判断层(早期命令)
│ └── config.py / render.py # 配置/终端渲染
├── profiles/f5.json # F5 字段映射
├── tools/ # 补测脚本
├── docker-build/ # Dockerfile + entrypoint(app/seed 构建时生成)
├── tests/ # 86 项测试 + 假 Graylog
├── docs/screenshots/ # 界面截图
├── data/ # 运行时数据(gitignore)
├── allowlist.example.txt
├── asn_brand_map.json
├── config.example.json
├── notify.example.json
└── README.md
```
---
## 开发与测试
```bash
# 全部单元 + 端到端测试(不需要真实 Graylog)
python3 -m unittest discover -s tests
# 用假 Graylog 预览各命令输出
python3 tests/demo.py
```
测试覆盖:字段推断、判断层、GeoIP/ASN、推送(含钉钉加签)、融合合并、推送退避、分页/搜索、监控指标、历史画像保留等。
提交前建议至少跑一次 `python3 -m unittest discover -s tests`。
---
## 已知限制
- **只读**:不下发封禁,不写 Graylog;封禁执行需对接 F5/AD/防火墙
- **单机 SQLite**:适合单实例;多实例部署需要换成共享数据库或只让一个实例写
- **面板鉴权**:默认无登录,靠监听地址/路径前缀/反代保护;公网暴露请务必设置随机 `--path-prefix` 并加 TLS/访问控制
- **行为候选上限**:行为分析只取 24h 访问量前列 + 4xx 较多 IP(约 600 条),极端大流量日可能漏掉长尾
- **证据依赖 Graylog 保留期**:日志过期后详情页回退到本地快照;更久的历史证据需要自行归档
- **自动补测上限**:缺口超过 7 天默认跳过,避免一次回扫过久
- **共享出口不封**:移动/教育网/CGNAT 的行为异常只告警,这是有意的策略
- **时区**:数据库和 API 使用 UTC,页面/推送使用本地时区;定时任务请按本地时区换算
---
## 排错
| 现象 | 处理 |
| --- | --- |
| 连接超时 | Graylog 端口通常是 **9002**;检查网络/VPN |
| 401 | token 错误或已吊销;确认是 API token |
| 403 | 账号缺少权限(`messages:read`、`searches:*` 等) |
| 页面一直“加载失败” | 查看 `docker logs` / 终端日志;确认 Graylog 可达 |
| 状态灯红色 | 最近一轮检测失败;看 `/api/status` 的 `last_error` |
| 钉钉推送 `310000 签名不匹配` | 机器人开了「加签」,补 `NOTIFY_SECRET=SEC...`,或用关键词/IP 白名单模式 |
| 名单没有新 IP | 先看 `/api/status` 和 `/api/monitor`;确认窗口内确有攻击流量;检查白名单/内网排除 |
| 启动没有自动补测 | `meta.detect_last_success` 不存在时会回退到 `ip_stats.updated_at`;缺口 ≤ 阈值或 > 7 天都会跳过 |
| 行为检测结果为空 | 24h 内没有达到 60 分且 ≥2 信号的 IP;或访问量太小(<10) |
| 页面分页空白 | 数据量变化后页码会自动回退;刷新即可 |
| Docker 启动后数据没变 | 旧数据卷优先;换新卷名或删除旧卷才会用镜像内 seed |
| 时间看起来差 8 小时 | DB/API 是 UTC,页面按本地时区展示;核对时注意时区 |
---
## 权限
只读 token 至少需要:
```
messages:read
searches:relative
searches:absolute
searches:keyword
streams:read
```
(`probe` 会逐项显示哪些接口可用。)
---
## 安全建议
- `graylog-ops.json`、`notify.json` 已加入 `.gitignore`,权限设为 `600`
- 公网分享时使用随机 `--path-prefix`,并通过反向代理加 TLS/访问控制
- 钉钉/企微 webhook 密钥不要提交到仓库;迁移后建议轮换一次
- 镜像不包含凭证,部署时用环境变量注入
- 定期备份 `data/ips.db`(SQLite 可用 `.backup` 或停服后复制)