AlexEreh/cvegochibot
GitHub: AlexEreh/cvegochibot
一个基于 Telegram 的漏洞情报订阅机器人,支持按关键词从多个来源聚合推送 CVE 信息,并具备跨来源去重和分话题独立配置功能。
Stars: 0 | Forks: 0
# cvegochibot
一个用 Go 编写的 Telegram 机器人,用于跟踪 CVE (NVD)、FSTEC BDU、CISA KEV 漏洞目录以及 GitHub Security Advisories 中与关键词(`nginx`, `iptables`, `nft`, ...)匹配的新漏洞。它在每个聊天中独立运行,而在启用了话题(Topics)的聊天中,则在每个话题中独立运行:每个聊天/话题都有自己的关键词集合和已启用的来源集合,可通过机器人命令进行配置。
## 功能
- 定期轮询漏洞来源,并将符合关键词的匹配结果分发到指定的聊天(以及话题,如果适用)。
- 完全基于 `(chat_id, topic_id)` 的独立配置:一个聊天/话题中的关键词和来源完全不会影响其他聊天/话题。
- 数据来源:
- **NVD CVE API 2.0** (`nvd`) — 通过 `lastModStartDate`/`lastModEndDate` 进行增量轮询。
- **FSTEC BDU** (`fstec`, bdu.fstec.ru) — 没有官方的增量 API,因此使用完整的 XML 导出(`vulxml.zip`,大约每天更新一次);采用流式解析,无需将完整的 XML(约 600 MB)加载到内存中。
- **CISA KEV** (`kev`) — 经证实正在被“在野”利用的漏洞目录;同样没有增量 API,但由于目录本身不大(约 1.5 MB),因此完全重新加载不是问题。
- **GitHub Security Advisories** (`ghsa`) — 软件包生态系统(npm, PyPI, Go, crates.io, Maven 等)的漏洞;通过 `updated` 进行增量轮询。
- **跨来源去重和分组**:如果多个已启用的来源报告了同一个漏洞(根据公共 CVE ID),通知只会发送一次,并列出提及该漏洞的所有来源(“另见于:...”),而不是每个来源发送一条单独的消息。
- 启用某个来源不会导致被历史记录“淹没”:仅处理在该来源被该聊天/话题启用后发生变更的漏洞(这主要适用于每次都全量返回数据的 FSTEC BDU 和 CISA KEV)。与关键词匹配的较旧记录将直接被标记为已处理,不再发送通知。
- 将修改类命令限制为仅聊天管理员可用(可配置,在私聊中无效);对代表群组发言的匿名管理员同样适用。
## 机器人命令
| 命令 | 描述 |
|---|---|
| `/start`, `/help` | 命令列表 |
| `/keywords` | 当前聊天/话题的关键词 |
| `/addkeyword [word2 ...]` | 添加关键词 |
| `/delkeyword [word2 ...]` | 删除关键词 |
| `/sources` | 可用及已启用的来源 |
| `/addsource ` | 启用来源 (`nvd`, `fstec`, `kev`, `ghsa`) |
| `/delsource ` | 禁用来源 |
| `/status` | 当前聊天/话题的配置 |
| `/settings` | 通过按钮进行设置(inline 键盘) |
| `/lookback` | 查找曾经与当前关键词匹配的所有漏洞(不仅是新漏洞),支持分页导航 |
当在聊天/话题中添加第一个关键词时,如果此前未显式配置过来源,系统将自动启用所有已注册的来源 — 您之后可以通过 `/delsource` 缩小范围。
### 按钮设置 (`/settings`)
除了文本命令外,`/settings` 还会打开一个 inline 键盘菜单 — 无需记住命令语法即可管理聊天:
- **🔑 关键词** — 当前词汇列表,每个词汇旁边都有一个 ❌ 按钮用于删除;**➕ 添加** 按钮会提示您在一条消息中以空格分隔发送新词并将其添加。
- **🗂 来源** — 每个来源旁的 ✅/⬜ 切换开关,点击即可立即启用/禁用。
- 整个菜单按话题(per-topic)工作,就像文本命令一样:如果带有 `/settings` 的消息是在论坛话题中发送的,所有操作都应用于该话题,而不是整个聊天。
- 更改设置的操作(添加/删除词汇、切换来源)会像相应的文本命令一样检查管理员权限(`admin.require_group_admin`)。
### 历史记录搜索 (`/lookback`)
常规通知仅包含在订阅来源*之后*发生的事件(请参阅前文的“如何对不同来源的漏洞进行分组” — 故意不发送历史积压数据)。`/lookback` 是一种显式查看此历史记录的方法:它在曾经与任何人的关键词匹配过的**所有**漏洞中搜索当前聊天/话题的关键词(表 `vulnerability_sources`),无论是否曾为其发送过通知。
搜索结果是一个紧凑的列表(带有链接的漏洞 ID + 简短描述 + 来源列表,每条记录一行),并在 ◀ 后退 / 前进 ▶ 按钮上提供分页导航(每页 10 条记录),而不是一长条消息或数十条单独的消息。它不需要管理员权限(只读),也不会向外部来源发出请求 — 仅对数据库中已积累的数据进行搜索。
## 架构
```
cmd/bot — точка входа, сборка зависимостей, graceful shutdown,
периодическая очистка БД (housekeeping)
internal/config — конфигурация (koanf): YAML-файл + переменные окружения
internal/models — доменные типы: Vulnerability (+ GroupKey для группировки
по CVE ID между источниками), Scope (chat+topic)
internal/storage — SQLite (modernc.org/sqlite, без CGO): ключевые слова,
источники, история уведомлений, курсоры провайдеров,
кросс-ссылки между источниками, Prune/Vacuum
internal/sources — интерфейс Provider + реестр
/nvd — адаптер NVD CVE API 2.0
/fstec — адаптер полного XML-экспорта БДУ ФСТЭК
/kev — адаптер каталога CISA KEV
/ghsa — адаптер GitHub Security Advisories
internal/scanner — фоновый опрос источников, сопоставление по ключевым
словам, дедупликация/группировка по CVE ID
internal/bot — Telegram-бот на Telego: команды, меню, форматирование, admin-guard
```
每个来源提供商都有自己的轮询间隔(NVD 使用 `scanner.poll_interval`,其他来源使用 `sources..refresh_interval`),与扫描器的全局 tick 相互独立。
### 如何对不同来源的漏洞进行分组
每条 `Vulnerability` 记录都带有 `CVEID` — 如果存在的话,即为 CVE 标识符(对于 NVD 和 CISA KEV,这是其自身的 ID;对于 FSTEC BDU 和 GHSA,则是来源指明的 CVE 交叉引用)。`GroupKey()` 会返回此 CVE ID,如果没有,则返回 `来源:ID` 对,因此没有公共 CVE 的记录永远不会被意外合并为一条。
扫描器正是通过 `GroupKey()` 而不是特定来源的 ID 将漏洞标记为“已见”(`seen_vulnerabilities`) — 因此,例如 NVD 和 FSTEC BDU 报告了同一个 CVE 时,通知只会发送一次(在首次发现时),而不是两次。此外,`vulnerability_sources` 表为每个 `GroupKey()` 存储了曾经报告过它的所有来源 — 这使得单次发送的通知可以包含发送时已知的所有来源的链接,而不仅仅是触发通知的来源。
## 快速开始
```
git clone && cd cvegochibot
cp config/config.example.yaml config/config.yaml
# 在 config/config.yaml 中输入来自 @BotFather 的 token,或者导出 BOT_TOKEN
go build -o bin/cvegochibot ./cmd/bot
./bin/cvegochibot -config config/config.yaml
```
通过 `make`:
```
make build
make run # соберёт и запустит с config/config.yaml
```
### 配置
设置从 YAML 文件读取(通过 `-config` 标志指定路径,默认为 `config/config.yaml`),并通过带有 `BOT_` 前缀的环境变量进行覆盖。最好不要将 token 存储在文件中 — 请通过 `BOT_TOKEN` 设置:
```
export BOT_TOKEN="123456:AA...your-token"
./bin/cvegochibot -config config/config.yaml
```
环境变量和默认值的完整列表请见 [`config/config.example.yaml`](config/config.example.yaml) 和 [`internal/config/config.go`](internal/config/config.go)。
### FSTEC BDU 和 TLS
`bdu.fstec.ru` 的证书是由俄罗斯国家根证书颁发机构(“Russian Trusted Root CA” / 俄罗斯数字发展部)签名的,该机构在大多数标准操作系统信任库中不存在。如果对 FSTEC 的请求因证书验证错误而失败 — 请将此根证书添加到操作系统/容器的信任库中,或者在 `sources.fstec.extra_ca_cert_path`(或 `BOT_FSTEC_EXTRA_CA_CERT_PATH`)中指定包含该证书的 PEM 文件路径。我们故意没有将此证书嵌入机器人的代码中 — 在无法与官方来源进行核对的情况下硬编码加密信任根字节是不安全的;请自行从俄罗斯数字发展部/国家服务官方门户网站获取并进行验证。
### GitHub Security Advisories 和 Token
如果没有 Token,GHSA 的轮询限制为 60 次请求/小时 — 这仅在 `refresh_interval` 较大时才够用。请在 https://github.com/settings/tokens 创建一个不带任何 scope 的 personal access token(仅用于获取更高的公开数据请求限额,5000次/小时),并在 `sources.ghsa.token` 中指定它(或者使用 `BOT_GHSA_TOKEN`,最好不要将 Token 存储在配置文件中)。
## 数据库大小
如果不加限制,`seen_vulnerabilities` 表(针对每个聊天/话题记录哪些漏洞已发送)和 `vulnerability_sources` 表(所有聊天共享的来源之间的交叉引用)将无限增长。相反,后台任务(`cmd/bot/main.go` 中的 `runHousekeeping`)会定期:
1. 从两个表中删除超过 `storage.retention`(默认 180 天)的行 — `storage.Store.Prune`。
2. 释放被删除行占用的磁盘空间 — `VACUUM`(`storage.Store.Vacuum`)。
这两个操作都可以通过 `storage.retention` / `storage.housekeeping_interval` 进行配置(默认每天一次;设置为 `0` 则完全禁用清理)。180 天留有非常大的余量:NVD 增量轮询的最大窗口为 120 天,而 FSTEC BDU 和 CISA KEV 每次都会完整返回当前状态(不依赖历史记录),而不是依赖数据库中过期的标记。
**预估大小。** 考虑到索引,每行 `seen_vulnerabilities`/`vulnerability_sources` 大约为 100–200 字节。最终大小 ≈(聊天话题数 × 每天平均关键词匹配数 × 保留天数) × ~150 字节。例如,100 个具有宽泛关键词的话题(每天约 20 次匹配)并保留 180 天,大约是 100 × 20 × 180 × 150 字节 ≈ 50 MB。如果要在更大规模(数百/数千个聊天)下将数据库保持在远小于 1 GB 的水平,请将 `storage.retention` 减少到例如 `720h`(30 天) — 这足以控制在 NVD/GHSA 的上限之内。
**为什么选择 SQLite 而不是其他。** 对于这种工作负载(单个 writer,简单的键值/关系查询,没有任何并发的外部客户端),SQLite 几乎是完美的选择:不需要单独的 DBMS 进程(减少了攻击面和运维负担),数据库文件本身就是备份(`cp bot.db bot.db.bak`),内置的 ACID 支持和索引都是免费的。像 PostgreSQL 这样完整的数据库服务器会增加运维复杂性(单独的进程、身份验证、网络访问、更新),这对于这种规模的 Telegram 机器人来说是完全不成比例的 — 仅当您已经在出于其他目的运行 Postgres 并希望整合基础设施时才值得使用。嵌入式键值存储(BoltDB 等)将迫使您手动实现在这里通过 SQL 免费获得的功能(按 `chat_id`+`topic_id` 查询、`DELETE ... WHERE seen_at < ?` 等)。结合上文提到的 Prune/VACUUM,SQLite 可以轻松地将该数据库控制在远低于 1 GB 的水平。
## 作为 systemd 服务部署
```
make build-static # статический бинарь для Linux в bin/cvegochibot
# (кросс-компилируется с любой ОС; для другой платформы:
# make build-static GOOS=linux GOARCH=arm64)
```
该项目完全没有任何 CGO 依赖(`modernc.org/sqlite` 是纯 Go 实现),因此 `CGO_ENABLED=0` 本身就能生成一个完全静态的、不依赖外部库的二进制文件 — `make build-static` 只是将其显式化,并添加了 `-trimpath -ldflags="-s -w"`(去除构建机器路径,去除调试符号)。
现成的 unit 文件位于 [`deploy/systemd/`](deploy/systemd/):[`cvegochibot.service`](deploy/systemd/cvegochibot.service)(文件顶部的注释中包含完整的安装步骤列表)以及限制服务 cgroup 内存/CPU/任务数的 [`cvegochibot.slice`](deploy/systemd/cvegochibot.slice)。服务以临时的非特权用户身份运行(`DynamicUser=yes`,无需手动 `useradd`),并进行了安全强化(`ProtectSystem=strict`,禁止获取新权限,限制 syscall 等)。密钥(`BOT_TOKEN`,可选的 `BOT_GHSA_TOKEN`)通过单独的 `EnvironmentFile`([`cvegochibot.env.example`](deploy/systemd/cvegochibot.env.example))传递,而不是放在 unit 文件本身中,以免在 `systemctl cat` 中暴露。
在生产环境使用之前,建议在真实的 systemd 环境中测试这些 unit(`systemd-analyze verify cvegochibot.service`) — 此仓库的代码是在 macOS 上开发的,无法在本地进行 `systemd-analyze` 验证。
## DevSecOps
该项目旨在防止不安全或损坏的代码被物理提交到代码库中。
### Git 钩子
```
make hooks # эквивалент: ./scripts/install-hooks.sh
```
这会将 git 指向版本化的 `githooks/` 目录(`core.hooksPath`),而不是将文件复制到 `.git/hooks/` 中 — 所有执行了 `make hooks` 的人拥有的钩子都是相同的。
- **`pre-commit`**(仅在提交包含 `.go` 文件时触发):`gofmt`, `go vet`, `golangci-lint`, `gosec`, `govulncheck`, `go test`。任何错误都将阻止提交。
- **`pre-push`**:`go build`, `go test -race` — 推送代码前对竞态条件进行最终检查。
紧急绕(不推荐):`SKIP_HOOKS=1 git commit ...`。
### SAST / SCA
| 工具 | 作用 | 手动运行 |
|---|---|---|
| [`golangci-lint`](https://golangci-lint.run/) | 通用 linter + 内置 `gosec`(配置见 [`.golangci.yml`](.golangci.yml)) | `make lint` |
| [`gosec`](https://github.com/securego/gosec) | Go 的专用 SAST | `make sast` |
| [`govulncheck`](https://go.dev/security/vulncheck) | SCA:标准库和依赖项中的已知漏洞,考虑代码的实际可达性 | `make sca` |
使用以下命令一键运行上述三个工具和测试:
```
make check
```
在提交时,`make check` 检查通过,没有任何问题(`golangci-lint`:0 issues,`gosec`:0 issues,`govulncheck`:0 可达漏洞)。依赖项是纯 Go,不包含 CGO(`modernc.org/sqlite`),这也简化了 SCA 和交叉编译。
Go 工具链通过 `go.mod` 中的 `toolchain` 指令锁定在修复了标准库已知漏洞的最新补丁版本;`govulncheck` 在每次运行时都会对此进行重新检查。
### CI
[`.github/workflows/ci.yml`](.github/workflows/ci.yml) 在每次 push/PR 时于 GitHub Actions 中重复相同的检查(build/vet/test,`golangci-lint`,`gosec`,`govulncheck`) — 钩子用于保护本地开发,CI 用于防范 `--no-verify` 和外部 PR。
## 测试
```
make test # unit-тесты
make test-race # то же самое с -race
```
单元测试涵盖了:关键词匹配、来源匹配和 `GroupKey` 分组(`internal/models`),配置加载与验证(包括真实的 `config.example.yaml`)(`internal/config`),存储层的 CRUD、按聊天/话题隔离以及 `Prune`/`Vacuum`(`internal/storage`),通过 `httptest` 对固定数据(fixtures)进行 NVD/GHSA 响应(包括基于 `Link` 标头的分页)以及 FSTEC XML 导出/KEV JSON 目录的解析(`internal/sources/...`),调度程序逻辑 — 匹配、去重、跨来源分组、提供商间隔(`internal/scanner`),以及安全的 HTML 转义和命令参数解析(`internal/bot`)。
特别通过回归场景测试(`TestScannerNotifiesNewEntryOnSubsequentFullRedownload`)验证了,对于没有增量 API 的来源(FSTEC,KEV),在后续的归档/目录完整重新下载中出现的真正新条目确实能够到达订阅者,而已知的条目不会被重复分发 — 既然这两个来源每次轮询时都会提供完整数据,这就必须能可靠地工作。
**测试限制:** 由于当前环境没有真实的 Telegram 机器人 token,因此未执行完整的端到端检查(实际向机器人发送命令、将通知实际发送到真实的聊天/话题)。我们能够使用真实的 Bot API 进行的测试是:使用语法正确但无效的 token 时,机器人在启动时会向 `api.telegram.org` 发出真实的 `getMe` 调用,并在遇到无效 token 时以明确的错误正常退出(用于启动时快速检查 token — 参见 `internal/bot/bot.go`),以及用于 long polling 的 `getUpdates` 调用 — 两者都确认发送到真实 API 的请求格式是正确的(服务器返回 `401 Unauthorized`,而不是请求格式错误)。所有基于 Bot API 工作的代码都在数据结构和逻辑层面得到了额外的单元测试覆盖。在生产环境使用之前,建议使用真实的机器人手动完整测试一遍主要命令。
## 已知限制
- FSTEC BDU 和 CISA KEV 不提供增量访问 — 每个轮询周期都会下载并解析当前完整的数据集(FSTEC 压缩后约 30 MB,KEV 约 1.5 MB),因此两者的 `refresh_interval` 都被故意设置得比较宽松(默认分别为每天一次和每 6 小时一次)。
- 关键词匹配是在标题、描述和受影响产品列表中进行不区分大小写的子字符串搜索;这不是基于 CPE 的完整匹配。
- 没有 `api_key` 的 NVD 限制为每 30 秒 5 次请求,没有 token 的 GHSA 限制为每小时 60 次请求;在存在大量聊天/话题的情况下,这不会阻碍轮询(轮询是全局的,而不是按聊天划分的),但建议在生产环境中获取密钥/token,特别是对于 `refresh_interval` 较短的 GHSA。
- 管理员权限检查使用 `getChatMember`,根据 Telegram 文档,“仅当机器人本身是聊天管理员时,才能保证对其他用户有效”。如果将机器人添加到群组中但未赋予管理员权限,而 `admin.require_group_admin` 已启用(默认启用),则权限检查可能不可靠。如果使用默认限制,请将机器人添加为群组管理员。
- 如果通知发送失败,机器人会进行几次带延迟的重试(最多 3 次),但不会无限重试:如果所有尝试都耗尽,对于轮询窗口严格递增的来源(NVD,GHSA),可能不会再重试该特定通知(日志中会有明确的 ERROR 级别记录)。对于每次轮询时全量重新加载数据的来源(FSTEC BDU,CISA KEV),这些通知自然会在下一个周期重新发送。
- 按 CVE ID 分组(参见前文“如何对不同来源的漏洞进行分组”)依赖于记录当前的 `GroupKey()`。如果记录最初出现时没有已知的 CVE(例如,FSTEC BDU 并不总是提供交叉引用),直到后来才获得 CVE 关联,这在技术上就是一个新的 `GroupKey()` — 理论上可能会因为同一漏洞的事实而再次发出告警。这是一个非常少见且极端的情况,项目未对此做单独处理。
标签:EVTX分析, Go, Ruby工具, Telegram机器人, XSS, 威胁情报, 开发者工具, 日志审计, 漏洞情报, 自动化通知