jjuhric/cvetrace-go
GitHub: jjuhric/cvetrace-go
一款零运行时依赖的 Go 语言 CVE 漏洞扫描器,支持多生态依赖检测并提供优先级排序和修复建议。
Stars: 0 | Forks: 0
# cvetrace-go
[jjuhric/cvetrace](https://github.com/jjuhric/cvetrace) 的 Go 移植版 —— 一个 CVE
**发现、追踪和解决方案** 的 CLI。理念相同,目标不同:此版本
编译为单一、完全静态的二进制文件,内部不捆绑任何运行时,无需安装任何东西即可运行,这与需要安装 Node 的 Node.js 原版(或像 Bun 这样将整个 JS 运行时打包进二进制文件,导致体积大得多的编译 JS 方法)不同。
**刚接触 Go?** 请参阅 [GO_PRIMER.md](GO_PRIMER.md) —— 这是一份概念图,将你在此项目的 Node 版本 (JavaScript) 中已了解的知识映射到 Go,并指出了每个概念在此实际代码库中出现的位置。源代码本身的注释也比通常更详细,专门用于解释 Go 惯用法。
## 为什么特别选择 Go
Go 直接编译为原生的、特定于操作系统的二进制文件,运行不需要单独的运行时 —— 与 C/C++/Rust 同属一类,比使用 Bun/Deno/Node 的 SEA 功能编译 JS 代码库(仍然将整个语言运行时打包到输出中,产生大得多的文件)高出一个层级。在这个特定项目中选择 Go 而不是 C/C++/Rust,是因为其标准库已经涵盖了 CVE 扫描器所需的一切 —— HTTP 客户端 (`net/http`)、JSON (`encoding/json`)、正则表达式、目录遍历 —— 无需任何第三方依赖,并且因为它可以从一台机器交叉编译为每个操作系统,且零额外工具 (`GOOS=windows go build`,无需调整工具链)。
## 环境要求
| 场景 | 需要什么 |
|---|---|
| 运行预构建的二进制文件 | 什么不需要。这就是重点 —— 请参阅 [安装预构建的二进制文件](#installing-a-prebuilt-binary)。 |
| 从源码构建/运行 | 已安装 [Go](https://go.dev) 1.26+ |
| 无论哪种方式 | 始终需要对 `api.osv.dev`(漏洞查询)的出站互联网访问。此外还需要安装 Java(用于 Gradle 本身),但仅当目标项目实际具有 `build.gradle`/`.kts` 时才需要 —— 否则不相关。 |
## 安装预构建的二进制文件
每个匹配 `v*.*.*` 的标签都会触发 [`.github/workflows/release.yml`](.github/workflows/release.yml),它为每个支持的操作系统/架构交叉编译此项目,并将二进制文件发布到该标签的 [GitHub Release](https://github.com/jjuhric/cvetrace-go/releases) —— 不需要 `go` 工具链,不需要 Node,在最终运行它的机器上无需安装任何东西。
1. 从[最新发布](https://github.com/jjuhric/cvetrace-go/releases/latest)下载与你的操作系统/架构匹配的二进制文件:
`cvetrace-windows-amd64.exe`, `cvetrace-darwin-amd64`, `cvetrace-darwin-arm64`,
`cvetrace-linux-amd64`, 或 `cvetrace-linux-arm64`。
2. 在 macOS/Linux 上,将其标记为可执行文件:`chmod +x cvetrace-*`
3. 直接运行它:`./cvetrace-darwin-arm64 scan ` (或者在 Windows 上只需运行
`cvetrace-windows-amd64.exe scan `)。
每次发布还会在二进制文件旁发布一个 `checksums.txt` (SHA-256),以便你
在运行下载的文件之前进行验证:`sha256sum -c checksums.txt`(或者与你下载的文件进行手动对比)。
`cvetrace --version` 会报告下载的二进制文件来自的确切发布版本 —— 请参阅
[`internal/cli/cli.go`](internal/cli/cli.go) 中的 `Version`,了解发布工作流如何在构建时将其植入其中。
## 用法
```
git clone https://github.com/jjuhric/cvetrace-go
cd cvetrace-go
go run ./cmd/cvetrace scan [options]
```
或者构建一次二进制文件并重复使用它:
```
go build -o cvetrace ./cmd/cvetrace
./cvetrace scan [options]
```
- `` — 要扫描的目录。检测 Node (`package.json`/
`package-lock.json`), Java/Maven (`pom.xml`), Java/Gradle (`build.gradle`/`.kts`,通过实际调用目标项目自己的 Gradle wrapper), 以及 Python
(`Pipfile.lock`/`requirements.txt`/`pyproject.toml`) —— 请参阅
[目前已实现的功能](#whats-implemented-so-far)以获取具体涵盖的内容。
- `--json` — 输出机器可读的 JSON 报告,而不是终端报告。
- `--fail-on ` — 如果发现达到或超过此严重程度的漏洞,则以非零状态退出
(`low`, `moderate`/`medium`, `high`, `critical`)—— 这是 CI pipeline 依赖的标志。被忽略的发现绝不会计入此项。
- `--exclude ` — glob 模式(相对于 ``),例如 `'test/**'`,
在依赖发现和代码使用扫描期间都要跳过 —— 可重复使用。
- `--ignore ` — 在此次运行中忽略特定的 CVE/GHSA/etc. id —— 可重复使用。如需
永久忽略,可以在扫描的目录中添加一个 `.cvetraceignore` 文件
(请参阅[忽略发现](#ignoring-findings))。
运行 `cvetrace scan -h` 获取完整的选项参考(示例、退出状态和
每个报告字段的说明)。
**Go 注意事项:** 与 Node 版本的 CLI(基于 commander.js 构建,可在任何地方接受 flags)不同,Go 的标准 `flag` 包通常要求 flags 出现在任何位置参数*之前*。本项目绕过了这一点(请参阅 `internal/cli/cli.go` 中的
`reorderFlagsFirst`),因此 `cvetrace scan --json` 和
`cvetrace scan --exclude test/**` 的工作方式与 Node 版本相同
—— 但如果你查看代码,这正是值得注意的那类陷阱。更多
详情请见 [GO_PRIMER.md](GO_PRIMER.md)。
## 忽略发现
有两种方法可以忽略已经过审查并接受的发现,使其不再显示(镜像
Nexus IQ/Dependabot 的“忽略”功能):
- `--ignore ` — 一次性的,仅限此次运行。
- 在要扫描的目录中放入一个 `.cvetraceignore` 文件 — 永久生效:每行一个 CVE/GHSA/etc.
id,忽略空行和整行的 `#` 注释,记录中会捕获可选的行尾
`# reason`。
```
# .cvetraceignore
CVE-2021-1234 # no reason given
CVE-2021-5678 # reviewed, false positive in our usage
```
被忽略的发现会从主报告中删除,并且不计入 `--fail-on`,
但绝不会默默丢弃它们 —— 使用 `--json` 运行以查看完整的 `ignored` 数组
(id、原因以及匹配的机制 —— `.cvetraceignore` 或 `--ignore`)。请参阅
`internal/trace/ignore.go`。
## 推荐的 agent 工作流
无论是人类还是 AI 编码 agent,都应该以相同的方式运行 cvetrace-go ——
`cvetrace scan --json` —— 但在项目 IDE 内工作的 agent (GitHub Copilot, Claude Code/Cowork,
Gemini Antigravity 等) 可以更进一步,直接对
报告采取行动。预期的循环:
1. 运行 `cvetrace scan . --json` (视情况添加 `--exclude`/`--ignore`/`--fail-on`)
并读取 `vulnerabilities` 数组 —— 已经按 `priorityScore` 排序,最高者
居首。按此顺序依次处理。
2. 对于每个发现,根据 `remediationTier` 进行分支处理:
- **`safe-to-update`** — 直接应用它。在 `manifestPath` 指定的文件中,将包升级到 `recommendedVersion`
(或者用 `fixedVersion`,仅解决这一个 CVE),或者 —— 如果 `dependencyScope` 为 `transitive` —— 改为应用 `overrideSnippet`
(它指明了确切的文件并给出了具体的代码片段:npm `overrides`,
Gradle `resolutionStrategy.force`, 或 Maven `dependencyManagement`)。然后运行
项目自有的 install/build/test 步骤以确认没有破坏任何东西,并重新运行
`cvetrace scan` 确认该发现已消失,然后再进行下一步。
- **`needs-approval`** — 暂时不要做任何更改。总结该发现(包,
当前版本 → 目标版本,为什么它是 major/breaking 升级)并提出简短的
实施计划 —— 需要修改哪些文件,可能会破坏什么,你将如何验证 ——
然后等待用户明确批准后再触及任何代码。
- **`no-fix-available`** — 目前还没有能解决此问题的版本升级。阅读
`advisoryDetails` 获取缓解/变通方法(例如配置标志),并提出
该方案,或者如果不存在变通方法,仅将其标记以引起用户注意。
- **`unknown-impact`** — 当作 `needs-approval` 同样处理:cvetrace 无法
确认版本跳跃的幅度,因此不要假设它是安全的。
3. 在反馈时,使用 `priorityScore`/`priorityLabel`, `usageContext`, 和
`codeReference` 来解释*为什么*某项发现会排在那个位置 —— 例如,“此 CRITICAL
CVE 为 P4 是因为它是仅在 dev 中的依赖,且未找到代码引用。”
4. 如果用户说跳过某项发现,记录该决定而不是
下次直接不提它:将它的 id 添加到项目中的 `.cvetraceignore` 文件中
(并加上说明由谁决定以及原因的 `# reason`)。
这是对预期工作流的描述,而不是 cvetrace-go 自身强制执行的 ——
如果你希望在该项目中工作的 agent 自动遵循它,请将其复制(或
改编)到你自己的项目的 agent 指令文件中
(`CLAUDE.md`, `AGENTS.md`, `.github/copilot-instructions.md` 等)。
## 工作原理
1. **发现** (`internal/discover`) — 遍历目标目录,跳过
`node_modules`/`.git`/`target`/etc.,并将每个生态系统的 manifest 解析为
已解析的 `{ecosystem, name, version}` 依赖项:Node 的 `package.json`/
`package-lock.json` (`node.go`), Maven 的 `pom.xml`(通过 Go 的内置 XML 解析器)
(`java.go`), Gradle 的 `build.gradle`/`.kts`(通过使用生成的 init script 实际调用目标项目
自己的 Gradle wrapper)(`gradle.go` —— 与 Node 版本使用的
“完全解析,而非静态解析”方法相同,如果无法调用 Gradle,则使用基于正则表达式的静态解析作为后备),以及 Python 的
`Pipfile.lock`/`requirements.txt`/`pyproject.toml` (`python.go`)。
2. **追踪** (`internal/trace`) — 批量向 [OSV.dev](https://osv.dev)(免费,无需 API
key)查询每个依赖项,针对当前版本实际落入的*特定*
受影响版本范围计算正确的修复版本(这是 Node 版本开发过程中的一个真实 bug,
在这里作为修复和回归测试被移植过来 ——
参见 `internal/trace/resolve.go` 中 `minimumFixedVersion` 的文档注释),合并 OSV.dev 有时为同一个 CVE 索引出的重复记录(另一个真实 bug,这次是在构建*此*移植版时发现的 —— 参见 `dedupeByCVE` 的文档注释),将每个修复的 semver 距离 (`updateImpact`) 和由此产生的单一可操作决策 (`remediationTier`) 进行分类,并将已知的、属于同一个包实例的每一个 CVE 聚合为一个 `recommendedVersion`。
3. **使用情况检测** (`internal/trace/usage.go`) — 对目标目录的
自有源文件(而非 manifest)进行第二次单独的遍历,通过正则表达式扫描该包的 import/require 语句,为每个发现的 `codeReference` 打标签。
4. **覆盖代码片段** (`internal/trace/override.go`) — 针对已知目标版本且确定为传递性的发现,生成确切的 npm/Gradle/(面向未来的
Maven) 代码片段,强制使用该版本,而无需等待父级更新。
5. **优先级** (`internal/trace/priority.go`) — 将严重程度、`usageContext` 和
`codeReference`(外加倾向于轻松获胜的小幅 `updateImpact` 权重)组合成一个
可排序的 `priorityScore` 以及 P1-P4 的 `priorityLabel`,并据此重新对每个发现进行排序
—— 这是 pipeline 的最终排序,特意采用了与
`severity` 不同的措辞,例如 "severity: CRITICAL, priority: P4"(位于仅在 dev 中的代码中的严重 CVE,在你自己的源码中从未被引用)读起来是一个合理的分类决策,而不是
矛盾。
6. **忽略过滤** (`internal/trace/ignore.go`) — 在报告之前,将 `.cvetraceignore` 与
`--ignore` 值合并,并将发现拆分为保留/忽略。
7. **报告** (`internal/report`) — 彩色终端报告,或 `--json`。
## 个发现的字段
| 字段 | 取值 | 含义 |
|---|---|---|
| `dependencyScope` | `direct` / `transitive` / `unknown` | 易受攻击的包是直接在你的 manifest 中声明的,还是由你依赖的其他内容引入的。 |
| `dependencyPath` | 数组或省略 | 对于**在 Node 或 Gradle 中**的传递性发现(这是此移植版解析真实依赖图的唯一生态系统):从直接依赖到这个包的链条,例如 `["webpack", "loader-utils", "vulnerable-pkg"]`。对于直接依赖省略,对于 Maven/Python 始终省略,因为这些根本没有进行传递性解析。 |
| `usageContext` | `production` / `development` / `unknown` | 该包是可以从你的生产依赖项中到达,还是仅能从绝不会发布的开发/测试/构建工具(`devDependencies`,Maven `test` 作用域,Gradle `testImplementation` 等)中到达。 |
| `codeReference` | `found` / `not-found` / `unknown` | 该包是否确实在你自己的源文件中的任何地方被 import/require,而不仅仅是在 manifest 中声明。**这是一个使用信号,而非可达性分析** —— `found` 并不意味着调用了特定的易受攻击的函数,而 `not-found` 也不能证明代码未被使用(忽略了动态 require、反射等)。Java/Kotlin 检测假设库的 import 与其 Maven/Gradle groupId 相匹配(通常如此,但不保证);Python 检测直接使用 PyPI 包名,这会遗漏那些 import 名称与发布名称不同的包(例如 PyYAML 是 `import yaml`)—— 这是一个已知差距,并未默默处理。请参阅 `internal/trace/usage.go` 中的 `DetectCodeReferences`。 |
| `updateImpact` | `patch` / `minor` / `major` / `unknown` | 修复所需的 semver 跳跃幅度有多大 —— 这是判断向后兼容可能性的启发式方法,而非保证。Log4Shell 自身的修复(2.14.1 → 2.15.0)按此衡量本身就是一个 "minor" 级别的升级。 |
| `recommendedVersion` | 版本或省略 | 在该确切包实例已知的每一个 CVE 中最高的单一修复版本 —— "升级到 X,清除一切",而不是去调和 N 个独立的逐 CVE 目标。如果目前没有任何已知修复,则省略。 |
| `advisoryDetails` | 文本或省略 | OSV.dev 的完整公告文本,除了“升级”之外,通常还包含缓解/变通方法部分(例如,Log4Shell 针对无法立即升级的用户的配置标志变通方法)。 |
| `remediationTier` | `safe-to-update` / `needs-approval` / `no-fix-available` / `unknown-impact` | 将 `fixedVersion` + `updateImpact` 融合为一个可直接据此分支处理的决策 —— 关于每个值能保证和不能保证什么,请参阅 `internal/trace/resolve.go` 中 `classifyRemediationTier` 的文档注释。显示为终端报告中的 `->` 动作行。 |
| `overrideSnippet` | 对象或省略(仅限 JSON 输出) | 对于具有已知目标版本的传递性发现:无需等待父依赖更新即可强制锁定修补版本的精确 `{file, instructions, snippet}` —— npm `overrides`,Gradle `resolutionStrategy.force`,或(已为 Maven 支持发展到该程度做好准备)Maven `dependencyManagement`。通常是对传递性 CVE 最快的真正修复方法。对于直接依赖以及完全不进行传递性解析的生态系统会被省略。参见 `internal/trace/override.go` 中的 `generateOverrideSnippet`。 |
| `priorityScore` / `priorityLabel` | 数字 / `P1`-`P4` | 结合了严重程度、`usageContext`、`codeReference` 以及小幅 `updateImpact` 加分的单一可排序的分类排名 —— 关于确切的公式,以及更重要的是它*不是*什么(它不是权威的风险评分),请参见 `internal/trace/priority.go` 中 `ComputePriority` 的文档注释。这是终端报告的实际排序顺序,也是每行上的 `[P#]` 前缀。 |
Node 版本的“补救智能”集中的每个字段现在都存在于这个
移植版中 —— 请参阅[目前已实现的功能](#whats-implemented-so-far)以获取完整的
逐生态系统图景。Node 版本 (`jjuhric/cvetrace`) 仍然是
确切行为的参考;本项目致力于将其忠实地移植到 Go,而不是
重新发明它。
## 目前已实现的功能
| 生态系统 | 读取的 Manifests | 状态 |
|---|---|---|
| Node.js | `package.json`, `package-lock.json` | 完全解析,包括传递依赖 —— `dependencyScope`/`usageContext`/`dependencyPath` 来自对 lockfile 自带的逐包 `dependencies` 图进行的广度优先遍历,以根 manifest 的 `dependencies`/`devDependencies` 作为种子(参见 `node.go` 中的 `buildNodeScopeMap`)。 |
| Java (Maven) | `pom.xml`, 包含 `${property}` 解析 | 仅追踪直接声明的依赖项 —— 没有传递解析(这需要调用 `mvn`,目前未实现),因此 `dependencyScope` 始终为 `direct`,并且始终省略 `dependencyPath`。`usageContext` 来自映射到 `development` 的 `test `,其他所有情况都映射到 `production`。 |
| Java (Gradle) | `build.gradle`/`.kts` | **完全解析**,通过使用生成的 init script 实际调用目标项目自己的 `gradlew`/`gradlew.bat` wrapper(如果不存在 wrapper,则回退到系统级的 `gradle`),包括传递依赖和通往每一个依赖的完整依赖路径 —— 准确度与 Node/npm 相同。如果无法调用 Gradle 本身(例如未安装 Java),则回退到对 `build.gradle` 进行基于正则表达式的静态解析(仅限直接依赖,无路径)。 |
| Python | `Pipfile.lock`, `requirements.txt`, `pyproject.toml` (尽力而为,不是完整的 TOML 解析器 —— 参见 `python.go`) | 没有传递解析 —— 始终省略 `dependencyPath`。对于 requirements.txt/pyproject.toml,`dependencyScope` 为 `direct`,对于 Pipfile.lock 则为 `unknown`(其 lock 格式不保留最初是声明还是通过传递方式引入的条目)。`usageContext` 来自 Pipfile.lock 的 default/develop 划分、类似 `requirements-dev.txt` 的同级文件名,或者符合类似 dev 命名约定(`dev`, `test`, `docs`, `lint`, `typing`)的 pyproject.toml/Poetry 组名。 |
## 开发
```
go build ./... # compile everything
go vet ./... # catch common mistakes static analysis can find
gofmt -l . # list any files that aren't formatted correctly (gofmt -w . to fix)
go test ./... # run all tests
```
为另一个操作系统交叉编译只需一个标志,无需额外工具链:
```
GOOS=windows GOARCH=amd64 go build -o cvetrace.exe ./cmd/cvetrace
GOOS=darwin GOARCH=arm64 go build -o cvetrace-mac ./cmd/cvetrace
GOOS=linux GOARCH=amd64 go build -o cvetrace-linux ./cmd/cvetrace
```
测试 fixtures 位于 `test/fixtures/*-fixture-project` 之下 —— 是 Node 代码库中同名
fixtures 的副本(`minimist@0.0.8`, `log4j-core@2.14.1` 即 Log4Shell(同时位于
`java-fixture-project` 的 `pom.xml` 中,以及单独位于 `gradle-fixture-project` 的
`build.gradle` 中),以及 `PyYAML==5.3`,每一个都是真实的、已知的 CVE),保持同步,以便两个项目都能针对完全相同的已知漏洞证明其正确性。Gradle
fixture 包含一个真实的、已提交的 Gradle wrapper,因此其测试会执行实际的 Gradle
调用,而不仅仅是静态解析的备用方案 —— 预计该特定测试将是测试套件中
最慢的(Gradle daemon/依赖缓存启动)。
### 发布版本
推送一个匹配 `v*.*.*` 的标签,然后 [`.github/workflows/release.yml`](.github/workflows/release.yml)
将接管后续工作 —— 作为关卡运行完整的测试套件,为每个支持的
操作系统/架构进行交叉编译,并将该标签植入作为 `cvetrace --version` 的输出,
并将二进制文件连同 `checksums.txt` 发布到该标签的 GitHub Release:
```
git tag v1.2.3
git push origin v1.2.3
```
## 项目布局
```
cvetrace-go/
go.mod # module definition (like package.json, but much smaller)
cmd/cvetrace/main.go # thin executable entrypoint
internal/
cli/ # argument parsing, subcommand dispatch
discover/ # finds dependencies in a project directory
trace/ # queries OSV.dev, resolves the correct fix version
report/ # terminal + JSON output
test/fixtures/ # known-vulnerable sample projects used by tests
```
请参阅 [GO_PRIMER.md](GO_PRIMER.md) 了解为什么要这样组织。
## License
MIT
标签:Claude, CVE检测, EVTX分析, Go, Homebrew安装, Ruby工具, 日志审计