kun9497/assay
GitHub: kun9497/assay
assay 是一个整合了 SBOM 生成与已知漏洞匹配的离线扫描工具,特色是支持韩国安全公告数据和提供可解释的匹配证据。
Stars: 1 | Forks: 0
# assay
*English · [한국어](README.ko.md)*
`assay` 从容器镜像、二进制文件或目录生成软件物料清单(SBOM)——或导入你已有的清单——并报告影响它的已知漏洞。
```
image / binary / dir / SBOM ──▶ package inventory ──▶ vulnerability match ──▶ verdict
```
## 状态
🚧 **早期开发阶段。目前尚未实现任何扫描功能。** CLI 仅搭建了基础框架:`assay version` 和 `assay help` 可以正常工作,而 `assay scan` 会刻意以退出码 2 退出,而不是报告一个它尚未实现的干净结果。
以下内容均为已达成共识的设计目标。[路线图](#roadmap) 追踪实际已构建的功能;[`docs/superpowers/specs/2026-07-29-assay-roadmap.md`](docs/superpowers/specs/2026-07-29-assay-roadmap.md) 包含了完整的设计以及每个决策背后的理由。
## 范围
`assay` 在一个工具中覆盖了完整路径:构建包清单、构建漏洞数据库,以及将两者进行匹配。在 anchore 生态系统中,这些是三个独立的项目 —— `syft`、`vunnel` + `grype-db` 和 `grype`。
现有的扫描器非常出色且经过实战检验;请在生产环境中使用它们。以下两点影响了本工具的开发:
- **韩国的安全公告数据。** KISA/KNVD 发布的安全公告和 KVE 标识符涵盖了 NVD 和 OSV 延迟收录或根本不收录的软件。主流扫描器并未导入这些数据。
- **可解释的匹配结果。** 每个发现都附带了生成它的证据 —— 哪个范围、哪个比较器、哪种比较结果 —— 而不仅仅是一个判定结论。
设计目标(按优先级排序):
1. **可解释** —— 每个发现都会说明它*为什么*匹配,而不仅仅是说明*是否*匹配。
2. **支持离线** —— 扫描时无需网络;`assay db update` 是唯一需要联网的命令。
3. **简洁无奇的输出** —— 确定性、可 diff、对 CI 友好。
配置扫描、机密扫描、IaC 以及 Kubernetes 安全态势都是明确的非目标。它们与漏洞匹配几乎不共享任何代码。
## 安装
```
go install github.com/kun9497/assay/cmd/assay@latest
```
或者从源码构建:
```
git clone https://github.com/kun9497/assay.git
cd assay
make build
```
## 使用方法
```
# 构建本地漏洞数据库 — 唯一需要网络的命令
assay db update
# 数据库中包含什么,以及数据的时效性
assay db status
# 扫描现有的 SBOM
assay scan sbom.cdx.json
# 扫描容器镜像
assay scan alpine:3.19
# 扫描二进制文件
assay scan ./bin/assay
# 扫描目录
assay scan dir:./my-project
# 在发现高危漏洞时使构建失败
assay scan alpine:3.19 --fail-on high
# ……以及在发现未评级漏洞时使构建失败(见下文)
assay scan alpine:3.19 --fail-on high --fail-on-unknown
# Machine-readable 输出
assay scan sbom.cdx.json --output json
```
在语义相同的地方,Flag 名称遵循 grype 的命名方式,因此你以前针对 grype 运行的任何命令在这里应该具有相同的含义。如果行为存在差异,都会将其记录在文档中,而不是让你自己去摸索发现 —— 目前 `--fail-on-unknown` 是唯一一个在 grype 中没有对应项的新增功能。
### 数据库
安全公告存储在本地,并在非扫描时间进行刷新。扫描过程绝不会下载任何内容:如果数据库缺失或其 schema 不匹配,`assay` 将以退出码 2 退出,并提示你运行 `assay db update`,而不是静默获取或静默地不报告任何内容。
| 操作系统 | 位置 |
|---|---|
| Windows | `%LocalAppData%\assay\db\v1\` |
| macOS | `~/Library/Caches/assay/db/v1/` |
| Linux | `~/.cache/assay/db/v1/` |
对于 CI 缓存或物理隔离环境,可使用 `ASSAY_DB_DIR` 进行覆盖。`v1` 组件代表 schema 版本 —— schema 发生变化时会重新构建到一个新目录中,而不是在原处进行迁移。
对于第一组生态系统(Go、npm、PyPI),预计磁盘占用约为 **86 MB** —— 包含 28,613 条安全公告,这是根据实时 OSV 转储(dump)测量得出的。初始的 `db update` 会下载约 244 MB 来生成该数据库:OSV 为每个生态系统发布一个归档文件,且没有服务端过滤,而 npm 的归档文件中大部分是恶意软件包报告,这些报告在导入时会被丢弃。
`assay db status` 会按 Provider 报告**上游数据**的最新时间 —— 而不是你碰巧下载它的时间。一个提供三个月前快照的镜像不应该仅仅因为你是在今天早上获取的,就显示为最新状态。
### 退出码
| 代码 | 含义 |
|-----:|---------|
| `0` | 扫描完成;没有任何项触发阻断机制 |
| `1` | 扫描完成;发现的问题达到或超过 `--fail-on` 标准,或在带有 `--fail-on-unknown` 时发现了未评级的问题 |
| `2` | 扫描无法完成,或其结果不可信 |
当同时适用多种情况时,**以最高优先级为准**:`2` 高于 `1` 高于 `0`。不可信的结果优先于结果的内容。
在 CI 中,区分“发现问题”和“无法运行”至关重要 —— 一个崩溃的扫描器绝对不应该看起来像是一次干净的构建。`assay` 无法评估的包会被报告为已跳过并附带计数,绝不会静默地将其归纳为干净的结论。
## 架构
该 pipeline 由五个接口组成。每个接口都可以独立测试,支持一个新的生态系统只需要编写一个 `Cataloger` 和一个 `Comparer` —— 其他部分无需更改。
| 接口 | 职责 | 实现 |
|---|---|---|
| `Source` | 打开目标以进行文件访问;携带层来源信息 | image, dir, file, binary |
| `Cataloger` | 文件 → `[]Package` | apk, dpkg, cyclonedx, go-mod, go-binary, npm, jar |
| `Store` | 安全公告查询 | bbolt |
| `Comparer` | 在单一生态系统内执行 `Compare(a, b string) (int, error)` | semver, PEP 440, apk, deb, rpm |
| `Provider` | 上游数据源 → `[]Advisory` | OSV, KISA |
数据库与扫描过程是正交的。`Provider` 通过 `assay db update` 填充数据库;扫描过程只负责读取。这就是使离线操作成为默认模式,而不是通过某个 flag 来实现的原因。
安全公告以 **OSV 结构** 存储 —— `affected[].ranges[]` 包含 `introduced` / `fixed` 事件。OSV 是主要的数据 Provider,几乎原封不动地透传数据;其他数据源则会被标准化为相同的格式。复用一个经过验证的标准化目标胜过自己凭空发明一个,而拥有自己的 schema 才使得 KISA 数据的引入成为可能。
记录以**无损方式**存储,派生值在查询时计算 —— 严重性等级来源于存储的 CVSS 向量,而不是在构建时就写死。目前还不需要的字段也会被存储下来,因为以后再添加字段意味着要重建整个数据库。
有两样东西在导入时会被丢弃。一是**已撤销的安全公告**,因为一个仍然会产生发现的已撤销公告完全就是误报。二是**恶意软件包报告**(`MAL-*`)—— 不是因为它们不重要,而是因为它们属于不同的发现类别:它们不携带严重性评级,不指明任何修复版本,并且要求“移除该组件并假设已被入侵”,而不是给出一个严重性评级。此外,它们大约占了原始数据的 80%。请参阅 [`docs/deferred-decisions.md`](docs/deferred-decisions.md) 了解若要妥善支持它们需要涉及哪些工作。
**大约一半的安全公告根本不携带严重性评级。** 强制将它们视为“低危”会把数据的缺失变成一次通过的构建 —— 这正是退出码旨在防止的失败情况。因此,`unknown` 有其独立的等级,位于 `low < medium < high < critical` 排序之外,而不是处于其最底层:
- 未知发现**总是会被报告**。阈值决定的是构建失败与否,而不是输出中显示什么,且未知的计数总是会在摘要中体现。
- 单凭未知(Unknown)**不会**触发 `--fail-on `。由于一半的安全公告未评级,如果设定为触发,那么每次扫描都会引发报错。
- `--fail-on-unknown` 对它们进行**显式**控制,因为上述两条规则依然会让未评级的高危漏洞以退出码 0 退出 —— 虽然被打印出来了,但构建是通过的。
### 三个以后容易弄错的地方
- **发行版软件包的生态系统键(ecosystem key)中包含其发行版本号** —— 即 `Alpine:v3.19`,而不是 `Alpine`。修复版本因发行版本而异,因此发行版本是查找键的一部分。
- **发行版属于目标(target),而不是软件包。** *镜像* 是 Alpine 3.19;而它包含的包本身不是。`Target.Distro` 通过读取一次 `/etc/os-release` 获取,并应用于其中的每一个 OS 软件包。
- **软件包会携带其源码包信息。** 发行版的安全公告是针对*源*软件包编写的,但实际安装的是*二进制*软件包 —— 如果去查找 `libssl1.1`,将永远找不到针对源码包 `openssl` 的安全公告。忽略这一点会产生漏报,而且是静默漏报。`Package.Source` 就是为了这种查找而存在的。这适用于 Alpine、Debian 以及 RHEL:Alpine 的 OSV 记录在其 purl 中带有 `?arch=source`。
### 为什么需要针对特定生态系统的版本比较
版本比较并不存在通用标准 —— Debian 的 epoch、RPM 的发行版排序、semver 的预发布优先级,以及 Maven 的排序规则彼此之间互不兼容。单一的 `compareVersions` 函数正是本设计极力避免的 bug 温床,这也是为什么 `Comparer` 是针对特定生态系统设计的。
`Comparer` 会返回错误,而不仅仅是返回排序结果,因为来自真实系统的版本字符串有时是格式错误的,如果将无法解析的版本视为“不受影响”,就会导致漏报。
## 路线图
按照可运行的路径进行切分,而不是按照架构层级 —— 单独的一层无法独立运行,而无法运行的设计就无法被验证。
**① 匹配核心** —— CycloneDX SBOM → 基于 OSV 的存储 → 匹配器 → 表格输出,适用于 Go、npm 和 PyPI。修复核心类型和接口。
- [ ] 核心类型,`Store` / `Comparer` / `Provider` 接口
- [ ] OSV Provider 和本地 bbolt 存储
- [ ] `assay db update`, `assay db status`
- [ ] CycloneDX SBOM 导入
- [ ] 基于生态系统的版本比较和范围匹配
- [ ] 表格输出
**② 容器** —— 拉取 registry、层提取、`/etc/os-release`、apk 目录化。设计风险最高,因此尽早进行而不是延后。
- [ ] 容器镜像扫描(优先支持 Alpine)
**③ 文件系统和二进制目标** —— 既不依赖于 ② 也不依赖于 ④,因此可以随时插入。
- [ ] 通过 `debug/buildinfo` 扫描 Go 二进制文件
- [ ] 目录扫描(Go modules)
**④ 判定与输出** —— 这是退出码 1 首次可达的地方。
- [ ] `--fail-on` 严重性阻断机制,外加针对未评级发现的 `--fail-on-unknown`
- [ ] JSON / SARIF 输出
- [ ] 解释模式 —— 显示单个发现的匹配证据
**⑤ KISA 数据补充** —— 将韩文描述、KVE 别名和严重性等级整合到匹配的发现中。
- [ ] KNVD Provider 和补充数据关联
在每个阶段,都会通过对 grype 进行**差分测试** 来检查正确性。虽然不期望达到完全一致 —— 毕竟数据源不同 —— 但如果出现巨大差异,就意味着匹配器出了问题。
二进制扫描的效果完全取决于编程语言留下了哪些信息。Go 和 Java 嵌入了足够的元数据来恢复依赖列表;Rust 只有在使用 `cargo-auditable` 构建时才会保留这些信息;而被 strip 过的 C/C++ 文件则什么也不会留下。是否提供支持是针对每种语言单独决定的,而不是作为一个笼统的承诺。
一些明显的缺失功能 —— 如 Debian 和 RHEL 支持、VEX 抑制、预构建的数据库产物、数据库时效性强制检查 —— 都是刻意为之的。
[`docs/deferred-decisions.md`](docs/deferred-decisions.md) 记录了哪些内容被推迟了,原因是什么,什么情况应该触发重新审视,以及已经奠定了哪些基础工作。
## 免责声明
这是一个独立的个人项目。它不隶属于任何雇主的产品,也未获得其认可或从中衍生而来,且不提供任何形式的保证。请勿将其作为你获取漏洞信息的唯一来源。
## 许可证
[Apache License 2.0](LICENSE)
标签:AI应用开发, EVTX分析, Go, Ruby工具, SBOM, 日志审计, 硬件无关, 韩国漏洞库