From 93e9f7416e08bd127c66ab56aba999490c23bc84 Mon Sep 17 00:00:00 2001 From: DeepFal Date: Wed, 21 Oct 2026 10:59:55 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E5=86=99=20README=EF=BC=9A=E5=AE=8C?= =?UTF-8?q?=E6=95=B4=E8=AF=B4=E6=98=8E=E7=89=B9=E5=BE=81/=E8=A1=8C?= =?UTF-8?q?=E4=B8=BA=E6=A3=80=E6=B5=8B=E3=80=81=E8=9E=8D=E5=90=88=E6=8E=A8?= =?UTF-8?q?=E9=80=81=E3=80=81=E7=9B=91=E6=8E=A7=E4=B8=8E=20Docker=20?= =?UTF-8?q?=E9=83=A8=E7=BD=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1219 ++++++++++++++++++++++++++++------------------------- 1 file changed, 638 insertions(+), 581 deletions(-) diff --git a/README.md b/README.md index 20d9824..3a54631 100644 --- a/README.md +++ b/README.md @@ -1,678 +1,735 @@ # graylog-ops -Graylog **4.0.x** 只读信息采集 + 封禁判断工具,服务于"用业务访问日志自动封 IP"这条链路。 +基于 **Graylog + F5 访问日志** 的恶意 IP 检测与封禁清单服务。 -## 边界(先说清楚) +它做三件事: -Graylog 是**旁路日志系统**:它能看到、能聚合、能告警,但**不在业务流量链路上,不能自己封 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) + +--- + +## 架构与数据流 ``` 外部客户端 │ ▼ -深信服 AD 7.0.8R5(应用交付 / 负载均衡 / 反代) - │ ▲ - │ └── 将来封禁的执行点大概率在这里(黑名单 / 访问控制) - ▼ -业务系统(nginx 等) ──日志──► Graylog ──► graylog-ops(本工具:采集 + 判断) - │ - └──► {ip, action, score, reasons} +F5(访问日志) ──syslog──► Graylog 4.0.x + │ API(只读) + ▼ + ┌──────────────────────────┐ + │ graylog-ops(单进程) │ + │ │ + │ 特征检测线程(60s) │ + │ 行为检测线程(15min) │ + │ 自动补测(启动时) │ + │ Web 面板 / API │ + └────────────┬─────────────┘ + │ SQLite(data/ips.db) + ▼ + 融合名单(建议封禁) + │ + ├── 钉钉/企微/飞书/Slack 推送 + ├── blocklist.txt 导出 + └── /metrics 监控 ``` -## 深信服 AD 在链路里的影响(关键) +Graylog 是旁路日志系统,本服务**只读**:不建流、不改索引、不下发封禁。封禁执行由 F5 / 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+ 标准库。 +### 环境要求 + +- Python **3.9+**(Docker 镜像使用 Python 3.12) +- 能访问 Graylog API(默认端口 **9002**) +- 可选:`data/geo/geo.db`、`data/geo/asn.db`(不提供也能跑,只是没有归属信息) + +### 配置 Graylog + +复制配置模板并填入 Graylog API token: ```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 +chmod 600 graylog-ops.json +# 编辑 graylog-ops.json:url / token ``` -> 认证方式:**API token 当用户名,字面量密码 `token`**(4.0 文档的写法)。 -> 工具内部已按此拼 `Authorization: Basic base64(:token)`,你只需要给 `--token`。 -> 非 GET 请求会自动带 `X-Requested-By`(Graylog 的 CSRF 保护要求)。 -> -> **token 放哪**:写进 `graylog-ops.json`(已在 `.gitignore` 里)并 `chmod 600`。 -> 只需要读权限的 token,**不需要管理员账号密码**;用完在 UI 里一键吊销。 -> 不要把 token 贴到聊天、工单或截图里——`--config` 读文件时它不会出现在命令行或输出中。 +也可以用环境变量(优先级:命令行 > 环境变量 > 配置文件): -## 命令 +```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 个信号才算行为违规): + +| 信号 | 加减分 | | --- | --- | -| `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 面板 | +| 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 | -通用参数:`--json`(机器可读)、`--out FILE`(写 JSON)、`-v`(打印每个 HTTP 请求)。 +行为违规后再结合 **ASN 出口属性** 给结论: -## 五步摸底 +| 出口属性 | 结论 | 是否进融合名单 | +| --- | --- | --- | +| 移动网络 / 教育网(共享出口) | 仅告警(共享出口) | 否 | +| 宽带 + 多 UA/多 Host(疑似多用户) | 仅告警(疑似多用户) | 否 | +| 宽带 + 单 UA 且行为分 ≥75 | 建议封禁 | 是 | +| 宽带 + 单 UA 且行为分 <75 | 建议人工确认 | 否 | +| 云主机 / IDC 机房 | 建议封禁 | 是 | +| 归属不明 | 建议人工确认 | 否 | -```bash -# 1) 能不能通、这个版本到底有哪些接口 -bin/graylog-ops probe +行为检测的候选范围:24 小时访问量前列 IP + 额外 4xx 较多的 IP,最多约 600 条进入画像;这是为了控制 Graylog 查询量,必要时可调整代码中的上限。 -# 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` 的输出(示例数据): +### 3. 融合名单 ``` -采样 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` +fused = 特征 ever_malicious=1 OR 行为 verdict = 建议封禁 ``` -## 判断规则(judge) +- **来源**:`feature`(特征)、`behavior`(行为)、`both`(双来源) +- **首次发现时间 `first_banned_at` 固定**:一个 IP 只会在进入名单时排一次序,后续再命中不会把它重新顶到最上面 +- **历史永久保留**:一旦进入过融合名单,即使后来不再活跃也不会被删除 +- **只有建议封禁会展示/推送**:其他行为结论只留在画像表里供详情页查看 -固定的 5 个 `terms` 查询 + 3 个统计量,不随 IP 数量增加查询次数: +### 4. 推送 -| 信号 | 查询(字段名来自 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 | — | 几乎全是失败 | +所有推送走**同一条融合链路**,同一 IP 不会因为多个来源收到多条消息。 -动作映射: +推送条件: -| 条件 | 动作 | -| --- | --- | -| 有攻击证据 且 score ≥ 75 | `ban_1h` | -| 有攻击证据 且 score ≥ 55 | `ban_30m` | -| score ≥ 40 | `observe`(只记录,不封) | -| 其余 | `ignore` | +- 该 IP 最近 24 小时内仍有活动(`last_activity_at`) +- 距离上次推送 ≥ 24 小时(`last_pushed_at`) +- 不在失败退避期内(`push_next_at`) -三条安全约束(比阈值本身更重要): - -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) +**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) ``` -**信号命中 0 个 IP 时要注意**:可能是该窗口确实没有这类流量,也可能是查询模板跟字段分词不匹配 -(比如 `request_uri` 在 ES 里的分词方式和你以为的不一样)。用 profile 覆盖 `query` 后再跑, -`judge` 会把每个信号的实际查询和命中数都打出来。 +- 首次部署/首次同步时,历史 IP 会被标记为已推送,不会补推 +- 推送失败:写入 `notifications` 表,按 **60s → 120s → 240s → … → 1h** 指数退避重试 +- 连续失败 3 次会记录告警;可选 `NOTIFY_ALERT_URL` 发到独立 webhook +- 详情链接前缀由 `link_prefix`(或 `NOTIFY_LINK_PREFIX`)控制 -## 持续检测服务(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/` -- 存储:`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/`、`/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: +本地配置:复制 `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://179.253.234.124:12139/s/301b.../", + "url": "https://oapi.dingtalk.com/robot/send?access_token=", + "secret": "SEC<加签密钥>", + "link_prefix": "http://:/s//", "title": "新增恶意 IP" } ``` -| type | 说明 | -| --- | --- | -| `wecom` | 企业微信群机器人(markdown) | -| `dingtalk` | 钉钉自定义机器人(markdown,支持 `secret` 加签) | -| `feishu` | 飞书自定义机器人(text) | -| `slack` | Slack Incoming Webhook | -| `generic` | POST `{"title","text"}`,可接自己的网关 | - -测试与自检: +不配置推送也能正常检测;需要验证 webhook 时用: ```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 +./bin/graylog-ops --config graylog-ops.json watch --notify-test --stream F5_Accesslog --profile profiles/f5.json --db data/ips.db ``` -推送内容示例: +### 5. 自动补测 -``` -**新增恶意 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) +每轮检测成功后会写入 `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 # 关闭自动补测 ``` -### 自动补测 / 推送审计 / 监控 +--- -- **启动自动补测**:启动时读取上次成功检测时间,缺口超过 30 分钟自动后台回扫(可用 `--backfill-threshold`、`--backfill-max-gap`、`--no-auto-backfill` 控制) -- **推送审计**:`notifications` 表记录每个 IP 的推送成功/失败、错误信息;失败按 60s→120s→…→1h 指数退避重试;连续失败 3 次会记录告警(可选 `NOTIFY_ALERT_URL` 发到独立 webhook) -- **监控**:页面右上角「监控」入口,展示 Graylog 状态、检测轮次/耗时、行为分析、推送成功率、名单和补测记录;`/metrics` 输出 Prometheus 格式,可直接被任意监控系统抓取 +## Web 界面 -### 状态指示灯 +### 列表页 -页面右上角有一个圆点,表示后端与 Graylog 的连接状态: +- 列:IP 地址 / 位置 · 归属 / 来源 / 评分 / 请求数 / 频率 / 发现时间 / 最近活动 / 判定理由 +- **位置 / 归属** 两行展示:第一行国旗 + 国家·省,第二行图标 + 厂商/运营商(悬停看 ASN 全名) +- **分页**:默认 20 条/页,可切 20 / 50 / 100;页码、上一页/下一页、总数 +- **搜索**:服务端搜索,支持 IP、国家/地区、厂商、ASN、来源、判定理由;250ms 防抖 +- 点击 IP 复制;点击行进入详情 +- 右上角状态灯:绿=正常、红=检测失败、黄=状态过期、灰=未知(悬停看提示) +- 每 30 秒自动刷新(可关闭) -- 绿色:最近一轮检测成功 -- 红色:最近一轮检测失败 -- 黄色:状态过期 -- 灰色:尚未开始检测 +### 详情页 -不展示文字,悬停可看提示。 +- 顶部:评分环、发现时间、最近活动、建议 +- **行为检测画像**:结论、人话理由、出口(厂商 + ASN + 类型)、24h 请求/失败率/URI/UA 数、信号列表 +- **判定依据**:命中的规则和次数 +- **证据**:请求频率分布、状态码分布、请求方法分布、访问域名分布、请求样本;证据按需从 Graylog 拉取并缓存,索引轮转后回退到本地快照 -### 行为检测(已并入主服务,单进程) +![详情页](docs/screenshots/detail-asn.png) -特征检测和行为检测现在跑在**同一个进程**里: +### 监控页 -- 特征检测线程:每 60 秒一轮(原有逻辑) -- 行为检测线程:每 15 分钟一轮(24h 窗口,纯行为评分,不依赖规则和名单) -- 同一个 Web 前端:融合首页 + 融合详情 +入口:页面右上角「监控」,或 `#/monitor`。 -**融合名单规则**: +展示: -- 只有「建议封禁」才展示和推送:特征命中攻击证据,或行为判定为建议封禁(非共享出口) -- 共享出口/NAT、人工确认、数据不足等行为结果**不展示、不推送** -- 一个 IP 只出现一行,来源标「特征 / 行为 / 双来源」 -- 首页按 `first_banned_at`(首次进入建议封禁的时间)倒序,**每个 IP 只置顶一次**,后续再命中不改变位置 -- 历史封禁 IP 永久保留在列表里(不参与 24h 再推) +- Graylog 连接状态、最近成功、距上次成功、最近错误 +- 特征检测轮次、失败次数、最近耗时 +- 行为分析次数、最近耗时、最近违规数 +- 推送 24h 成功率、成功/失败数、连续失败、最近错误 +- 名单总数、今日新增、双来源、行为新发现 +- 自动补测次数、上次时间/区间/新增 +- 最近推送记录(成功/失败 + 错误) -**推送**: +### 复制 / 导出 -- 融合成一条消息,同一 IP 永远不重复推 -- 同一 IP 24 小时内最多推一次;24 小时后仍在活动(最近 24h 有流量)的 IP 会再推一条 -- 首次部署时历史 IP 静默(不补推) +- 「复制 IP」:逗号分隔 / 换行分隔 +- 「导出」:CSV(完整字段) / 仅 IP 列表 +- 都作用于**当前搜索条件下的全部记录**,不是只导出当前页 -**详情页**:同一 IP 融合展示「行为检测画像」和「特征检测证据」。 +--- -相关参数:`--behavior-interval 900`(行为刷新间隔)、`--no-behavior`(关闭行为检测)。 +## HTTP API -### ASN / IP 归属 +服务启动后(假设无路径前缀): -- 列表「位置 / 归属」合并为一列、上下两行:第一行国家/地区,第二行图标 + 厂商/机构(如 ☁️ 阿里云、📱 中国移动) -- 悬停显示 ASN 号、完整组织名和网络类型 -- 详情页显示 ASN 号 + 厂商 + 组织全名 + 类型 -- CSV 导出包含 `asn, asn_org, asn_brand, asn_type` -- 品牌映射在 `asn_brand_map.json`(可继续加厂商),数据库 `data/geo/asn.db` -- 更新方式(DB-IP ASN Lite,每月): +| 接口 | 说明 | +| --- | --- | +| `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 +python3 -m graylog_ops.asn \ + --csv data/geo/dbip-asn-lite-2026-10.csv \ + --out data/geo/asn.db ``` -关闭 ASN:`--asn-db ""` +新增厂商:在 `asn_brand_map.json` 的 `items` 里加关键词、品牌名、类型、图标即可,不用改代码。 -### 名单语义(重要) +类型取值:`cloud`(云服务商)、`idc`(IDC/机房)、`mobile`(移动网络)、`broadband`(宽带)、`education`(教育网)、`enterprise`(企业/政府)、`unknown`(未知)。 -- **永久保留**:一旦某个 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/方法/状态码等字段不会再把乱码带进页面。 +关闭:`--asn-db ""` -### 启动方式(按需,不挂后台服务) +--- -按当前约定**不安装 launchd/systemd**,需要看的时候手动起(DSH/终端重启后进程会消失,重新执行即可): +## 目录结构 + +``` +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 -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 +# 全部单元 + 端到端测试(不需要真实 Graylog) +python3 -m unittest discover -s tests + +# 用假 Graylog 预览各命令输出 +python3 tests/demo.py ``` -停止:Ctrl+C(或用 `pkill -f "graylog_ops watch"`)。 +测试覆盖:字段推断、判断层、GeoIP/ASN、推送(含钉钉加签)、融合合并、推送退避、分页/搜索、监控指标、历史画像保留等。 -维护命令: +提交前建议至少跑一次 `python3 -m unittest discover -s tests`。 -```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:` | - -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` 看量。 +- **只读**:不下发封禁,不写 Graylog;封禁执行需对接 F5/AD/防火墙 +- **单机 SQLite**:适合单实例;多实例部署需要换成共享数据库或只让一个实例写 +- **面板鉴权**:默认无登录,靠监听地址/路径前缀/反代保护;公网暴露请务必设置随机 `--path-prefix` 并加 TLS/访问控制 +- **行为候选上限**:行为分析只取 24h 访问量前列 + 4xx 较多 IP(约 600 条),极端大流量日可能漏掉长尾 +- **证据依赖 Graylog 保留期**:日志过期后详情页回退到本地快照;更久的历史证据需要自行归档 +- **自动补测上限**:缺口超过 7 天默认跳过,避免一次回扫过久 +- **共享出口不封**:移动/教育网/CGNAT 的行为异常只告警,这是有意的策略 +- **时区**:数据库和 API 使用 UTC,页面/推送使用本地时区;定时任务请按本地时区换算 -## 与执行侧衔接(下一步) - -判断层已经产出 `{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` 文件 | +| 连接超时 | 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` 或停服后复制)