# 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://:/s//#/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=", "secret": "SEC<加签密钥>", "link_prefix": "http://:/s//", "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 详情(含行为画像、融合记录、证据) | | `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` 或停服后复制)