736 lines
28 KiB
Markdown
736 lines
28 KiB
Markdown
# graylog-ops
|
||
|
||
基于 **Graylog + F5 访问日志** 的恶意 IP 检测与封禁清单服务。
|
||
|
||
它做三件事:
|
||
|
||
1. **特征检测**:用 12 条规则识别扫描器、漏洞探测、登录爆破、恶意 UA 等攻击行为;
|
||
2. **行为检测**:独立于规则和名单,按 24 小时访问行为给 IP 打分;
|
||
3. **融合输出**:只保留"建议封禁"的 IP,形成一个去重、可分页、可推送的清单,并提供监控面板。
|
||
|
||
服务是**单进程**的:检测线程 + 行为线程 + Web 面板跑在同一个进程里,数据落在本地 SQLite。
|
||
|
||

|
||
|
||

|
||
|
||
---
|
||
|
||
## 功能特性
|
||
|
||
- **特征 + 行为双检测**,融合后按 `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 拉取并缓存,索引轮转后回退到本地快照
|
||
|
||

|
||
|
||
### 监控页
|
||
|
||
入口:页面右上角「监控」,或 `#/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` 或停服后复制)
|