28 KiB
graylog-ops
基于 Graylog + F5 访问日志 的恶意 IP 检测与封禁清单服务。
它做三件事:
- 特征检测:用 12 条规则识别扫描器、漏洞探测、登录爆破、恶意 UA 等攻击行为;
- 行为检测:独立于规则和名单,按 24 小时访问行为给 IP 打分;
- 融合输出:只保留"建议封禁"的 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:
cp config.example.json graylog-ops.json
chmod 600 graylog-ops.json
# 编辑 graylog-ops.json:url / token
也可以用环境变量(优先级:命令行 > 环境变量 > 配置文件):
export GRAYLOG_URL=http://graylog.example:9002
export GRAYLOG_TOKEN='你的 API Token'
Token 认证方式是 token 当用户名、字面量 token 当密码,工具内部已处理,不需要额外配置。
只读权限即可:
messages:read、searches:*、streams:read等,详见文末「权限」。
启动服务
./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_* 环境变量。示例:
{
"type": "dingtalk",
"url": "https://oapi.dingtalk.com/robot/send?access_token=<token>",
"secret": "SEC<加签密钥>",
"link_prefix": "http://<host>:<port>/s/<secret>/",
"title": "新增恶意 IP"
}
不配置推送也能正常检测;需要验证 webhook 时用:
./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 天:跳过并打印日志
自动补测会:
- 按规则回扫缺口时间段(自动做时间切片,避免聚合条数上限漏 IP)
- 跑一次该时间段的行为分析(仅缺口 1–48 小时时)
- 补录的 IP 全部抑制推送,不会补发钉钉
参数:
--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/来源/理由) |
返回:
{
"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 核验 |
维护/补测工具
# 特征检测补测(支持自动时间切片)
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;已有数据卷则继续使用卷内数据。
构建
# 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 不会打进镜像。
运行
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 段) - 列表显示中文国家名 + 省/州;详情显示城市
- 更新(每月):
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+ 条关键词 → 厂商/运营商 + 图标 + 类型) - 更新:
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
开发与测试
# 全部单元 + 端到端测试(不需要真实 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或停服后复制)


