Corvalon/lichen
GitHub: Corvalon/lichen
一款本地、免费、支持离线的跨语言依赖情报工具,用统一的输出格式和 CI 拦截机制覆盖八大包管理生态系统的许可证、CVE、版本升级分析。
Stars: 0 | Forks: 0
# lichen
一款本地、免费的依赖情报工具,支持 **Go、npm、Cargo、Python、Ruby、Maven、Gradle 和 NuGet**。对于构建的依赖图中的每一个包,lichen 会显示**锁定版本**、**每个父级实际要求的版本**、**许可证**、已知的 **CVE**、**发布日期**、**逐版本的升级路径**,以及**具备安全意识的推荐升级**——所有这些都在你的本地机器上完成,无需账号,无需商业后端,并且支持离线使用。
## 为什么选择 lichen
确切了解你实际依赖的内容,以及何时可以安全地进行更新,往往比想象中要困难得多:
- 包管理器自身的列表(`go list -m all`、`npm ls`、`cargo tree`、`mvn dependency:tree`、`bundle list`)只显示锁定版本,而不显示许可证、CVE,或者是谁引入了这个依赖。
- 每个生态系统对这个问题的回答都不一样,因此一个多语言仓库需要为每种语言配备不同的工具、不同的输出格式以及不同的 CI 步骤。
- **商业 SCA 套件**能够回答这些问题,但完整的功能集需要付费,而且分析过程通常通过供应商的后端进行——你的依赖树就这样离开了你的掌控。
- 特定语言的*可达性*工具(例如用于 Go 的 `govulncheck`)在这项工作上做得很好,但它们只回答了一个问题(是否调用了易受攻击的符号)——而不是“我的许可证风险有多大,哪些过时了,哪个升级能修复这个 CVE,以及这个升级是否安全”。
而在这一切的背后,隐藏着一个时机问题:**最危险的包版本往往是刚发布一小时的那个版本。** 被劫持的维护者和被投毒的发布通常会在几天内被发现并撤销,然而自动升级工具却会争先恐后地让你用上零日发布的版本,而免费技术栈中几乎没有什么东西会根据*发布时长*进行拦截,或者引导你升级到经过最新**审查**的修复版本,而不是盲目使用最新发布版本。lichen 将发布的新鲜度视为一种策略:一个可配置的“浸泡”窗口,CI 拦截和升级建议都会遵守它(参见策略 / CI 拦截下的[“为什么需要浸泡窗口?”](#policy--ci-gating))。
lichen 通过一个 **100% 免费的技术栈**填补了这些空白:使用语言本身在本地的构建产物(参见下方的生态系统表格)以及公共的 [deps.dev](https://docs.deps.dev/api/v3/) API(许可证、版本、安全公告;无需身份验证)。**一个工具,一种输出格式,一个由单一退出代码控制的 CI 拦截机制,覆盖所有八个生态系统。** 生态系统会根据现有的 lockfile 自动检测(可以使用 `--ecosystem` 覆盖);多副本解析(npm、Cargo、uv)采用原生建模,每条边都携带了父级声明的**范围**,并且升级建议被限制在每个父级可接受的最高版本。
| 生态系统 | 检测方式 | 来源 | 运行时工具链 |
| --- | --- | --- | --- |
| Go | `go.mod` | `go mod graph` + `go list` | `go` |
| npm | `package-lock.json` | 已提交的 lockfile | 无 |
| Cargo (Rust) | `Cargo.lock` | 已提交的 lockfile | 无 |
| Python | `uv.lock` / `poetry.lock` | 已提交的 lockfile | 无 |
| Ruby | `Gemfile.lock` | 已提交的 lockfile | 无 |
| NuGet | `packages.lock.json` | 已提交的 lockfile | 无 |
| Maven | `pom.xml` | `mvn dependency:tree -Dverbose` | `mvn` |
| Gradle | `gradle.lockfile` | 已提交的 lockfile(Maven 坐标) | 无 |
只有 Go 和 Maven 会调用其构建工具;其他所有的适配器都是解析已提交的 lockfile,因此在物理隔离或 CI 环境中无需安装任何工具链。每个生态系统都有一个专门的版本比较器(Go/npm/Cargo 使用 semver,Python 使用 PEP 440,Ruby 使用 Gem::Version,Maven 使用 ComparableVersion,NuGet 使用 SemVer-2-plus-revision),因此“过时”和“最新”绝不是简单的字符串排序。
**lichen 负责拦截;它不处理可达性。** 请保留你的生态系统调用图工具(Go 使用 `govulncheck`,其他地方使用类似工具)来进行符号级的可达性分析,并将 lichen 保留为具备离线能力、跨生态系统的**策略拦截器**(许可证 + 陈旧度 + 安全公告 + 新鲜度)和**升级顾问**。它们相辅相成。
## 运行效果
交互式 HTML 报告(`lichen . --json | node tools/dist/viz.js`),此处使用内置的离线测试数据(`npm run viz:demo`)进行渲染。每个依赖占一行:锁定版本、父级要求的版本、许可证、链接到 OSV 的安全公告标签,以及展开的升级路径(包含逐个版本的发布日期、许可证和具备安全意识的建议)。严重性过滤器和名称过滤器可将树状图折叠为仅显示重要的路径;该页面是一个单一的自包含文件,并会跟随你的浅色/深色偏好。
## 每个依赖你能获得什么
| | |
| --- | --- |
| **锁定版本** | MVS/解析机制实际为你构建选定的版本 |
| **父级约束** | *父级*声明的版本,单独显示,以便你能看出解析机制何时将一个间接依赖提升到了父级要求的版本之上 |
| **许可证** | 来自 deps.dev 的 SPDX;在升级时会标记许可证变更(例如 MIT -> BUSL) |
| **安全公告** | OSV/CVE id、CVE 别名、CVSS 分数、标题、链接 |
| **可用版本** | 完整的已发布列表;包含每个版本的发布日期 |
| **推荐升级** | 安全 > 许可证 > 符合条件的最高版本,并说明其权衡取舍 |
| **范围** | `production` / `test` / `not-imported`(在生态系统元数据支持的情况下,见下文)。`not-imported` 依赖仅供参考,永远不会触发拦截,因此没有人 import 的依赖中的 CVE 不会导致构建失败 |
| **语言版本** | 构建的语言/工具链要求,当存在更新的稳定版本时会进行标记(目前**仅支持 Go**,通过 [go.dev](https://go.dev/dl/) 读取 `go` / `toolchain` 指令获取) |
**各生态系统的范围解析。** Go 是最精确的:`go list -deps ./...` 与 `go list -test -deps ./...` 将图分为三部分,存在于 `go mod graph` 中但不在任一 import 集合中的任何内容都是 `not-imported`(依赖自身的测试依赖,不会被编译进你发布的任何内容中)。npm、Maven 和 poetry 根据其 dev/test 组的元数据来划分 `production` 和 `test`。其余适配器(Cargo、uv、Ruby、NuGet、Gradle)将范围设为 **unknown**,并且未知范围的拦截行为与 `production` 完全一致——lichen 绝不会做出可能会默默抑制发现的猜测分类。
## 安装
lichen 是一个 Node CLI(需 Node 18+)。通过 npm 安装:
```
npx @corvalon/lichen # one-shot run, no install
npm i -g @corvalon/lichen # or install the `lichen` command globally
```
VS Code 扩展可以从 [Releases 页面](https://github.com/Corvalon/lichen/releases/latest)下载打包好的 `.vsix` 文件进行安装。下载最新的 `lichen-vscode-*.vsix`,然后运行:
```
code --install-extension lichen-vscode-0.4.10.vsix # use the version you downloaded
```
或者在编辑器中,打开扩展视图,点击 `...` 菜单,然后选择 **Install from VSIX** 并选择下载的文件。这适用于任何兼容 VS Code 的编辑器(VS Code、VSCodium、Cursor、Windsurf)。
或者从源码构建:
```
git clone lichen && cd lichen
npm install && npm run build # builds the engine/CLI -> dist/, and tools/
```
这会生成 `dist/cli.js`,直接通过 `node dist/cli.js` 运行。
要获取真正的 `lichen` 命令(而不是 `node dist/cli.js`),请将其全局链接:
```
npm link # symlinks `lichen` onto your PATH
lichen --offline # now runnable as `lichen`
```
下面的示例使用的是 `node dist/cli.js`;如果你已经安装或链接了它,请替换为 `lichen`(包名是带作用域的 `@corvalon/lichen`,但安装后的命令就是纯粹的 `lichen`)。
## 快速开始
```
node dist/cli.js # scan the current dir, print the text tree
node dist/cli.js ~/code/myapp # scan another project (ecosystem auto-detected)
node dist/cli.js . --ecosystem NPM # force one, if a repo has several manifests
```
生态系统是根据现有的构建文件(`go.mod`、`package-lock.json`、`Cargo.lock`、`uv.lock` / `poetry.lock`、`Gemfile.lock`、`packages.lock.json`、`pom.xml`、`gradle.lockfile`)来确定的。无论它选择哪一个,输出格式、`--json` schema、SBOM、策略文件和退出代码都是完全相同的。
## CLI 参考
`node dist/cli.js [路径] [标志]`(路径默认为 `.`)。标志按使用频率分组如下:
**日常使用**
| 标志 | 效果 |
| --- | --- |
| *(位置参数)* `path` | 要扫描的目录;生态系统会根据现有的 lockfile 自动检测(默认为 `.`) |
| `--ecosystem ` | 强制使用某个适配器而不是自动检测,适用于包含多个清单的仓库。接受 `go`、`npm`/`node`、`cargo`/`rust`、`python`/`pypi`/`uv`/`poetry`、`ruby`/`gem`/`bundler`、`maven`/`mvn`/`gradle`、`nuget`/`dotnet` |
| `--json` | 以 JSON 格式打印富集后的树(通过管道传递给可视化工具或 CI)。携带一个顶层 `schemaVersion` 供使用者依赖(仅在发生破坏性变更时递增) |
| `--format ` | 输出格式:`tree`(默认)、`json` 或 `cyclonedx`(一个包含组件 + 漏洞的 CycloneDX 1.6 SBOM,可供 Dependency-Track 等工具摄取)。`--json` 是 `--format json` 的简写 |
| `--offline` | 跳过 deps.dev:仅保留树结构 + 锁定版本 + 父级约束 |
| `--upgrades` | 同时获取每个较新版本的许可证 + 安全公告(支持逐版本的升级列表和具备安全意识的推荐) |
| `--critical ` | 在选择推荐升级时,CVSS 高于 `n` 的视为“关键”(默认 `7.0`;`0` 表示将任何有评分的安全公告视为关键)。与可视化工具的 `--critical` 相对应 |
| `--config ` | 组织**基础**策略;被扫描仓库的 `lichen.config.json` 会作为局部覆盖层叠加在其之上,且只能**收紧**基础策略(如果不存在仓库配置,则单独使用) |
| `--version` / `-V` | 打印版本并退出 |
| `--help` / `-h` | 打印用法摘要并退出;在子命令后也可使用(`lichen apply --help`) |
无法识别的 `--flag` 是一个**硬错误**(退出代码 `1`),绝不会默默忽略,因此像 `--offlien` 这样的拼写错误会明确报错,而不是使用默认值进行扫描且看起来好像正常运行了一样。
**测试 / 物理隔离**(提供保存的 API 输出而不是调用 deps.dev,提供保存的命令输出而不是运行构建工具)
| 标志 | 效果 | 生态系统 |
| --- | --- | --- |
| `--facts-file ` | 预先准备好的每个包的事实(离线许可证/安全公告/版本富集)。接受 `lichen facts` 快照(带版本号的 `{schemaVersion, facts}` 封装)或传统的裸 map | 全部 |
| `--graph-file ` | 预先准备好的 `go mod graph` 输出(与 `--list-file` 一起使用) | Go |
| `--list-file ` | 预先准备好的 `go list -m all` 输出 | Go |
| `--deps-file ` | 预先准备好的 `go list -deps` 输出(设置了 production 范围);如果省略,范围保持未知 | Go |
| `--test-deps-file ` | 预先准备好的 `go list -test -deps` 输出(设置了 production + test 范围) | Go |
提供这些图捕获标志的原因是 **Go 和 Maven 是唯一会调用构建工具的适配器**。其他六个适配器读取的是已经提交到仓库中的 lockfile,因此在离线机器上,图根本不需要捕获步骤:只需检出代码库并添加 `--facts-file` 即可。参见[物理隔离的 CI](#air-gapped-ci)。
退出代码:`0` 正常,`1` 硬错误,`2` 策略违规。
### 子命令
除了默认的扫描功能,`lichen` 还有五个动词。`apply` 会进行修改(需通过 `--yes` 开启);`impact`、``、`facts` 和 `report` 都是只读的。
| 命令 | 功能 |
| --- | --- |
| `lichen apply [path]` | 将依赖升级到推荐的修复版本(参见[执行升级](#apply-upgrades)) |
| `lichen report [path] [-o out.html]` | 生成交互式 HTML 报告(参见[可视化](#visualize)) |
| `lichen impact [@] [path]` | **正向爆炸半径**:通过 Go MVS 机制,展示版本提升会向上强制引发什么变更,并附带每个强制变更节点的许可证变更/CVE 差异。如果存在策略(`lichen.config.json` / `--config`),它还会通过 `!! GATE` 行**标记引入拦截违规的升级**(新拒绝的许可证或超标的 CVE)。省略 `@` 可升级到已知的最新版本。使用 `--json` 进行机器输出;使用 `--requirements-file` 进行完全离线的运行 |
| `lichen why [path]` | **反向查询**:将模块锁定在其最新版本之下的父级要求,以及解除该限制的命令 |
| `lichen facts [path]` | 输出当前构建 deps.dev 数据的版本化 `{schemaVersion, facts}` 快照,以便通过 `--facts-file` 提供给物理隔离的拦截器使用(参见[物理隔离的 CI](#air-gapped-ci)) |
```
# 将 shared 升级到 v1.7.0 会带来什么?
node dist/cli.js impact github.com/acme/shared@v1.7.0
# 目标:shared v1.5.0 -> v1.7.0 [重新授权 MIT -> BUSL-1.1](清除 CVE-2024-9999)
# 强制:leaf v0.4.0 -> v0.5.0 (清除 CVE-2024-1111)
# 为什么 shared 卡在 v1.5.0?
node dist/cli.js why github.com/acme/shared
# 因 github.com/acme/parent-b 需要 v1.5.0 而被固定在 v1.5.0
# 要解除限制:go get github.com/acme/shared@v1.7.0
```
`impact` **仅适用于 Go**;其他所有命令均适用于全部八个生态系统。正向遍历是一种受限的重新解析,依赖于 MVS 的单选定版本属性(在整个构建中每个模块只有一个锁定版本,并且版本只会向上移动),这是 Go 提供的保证。通过范围解析或允许包存在多个副本的生态系统需要不同的算法,因此 `impact` 在非 Go 的依赖树上会明确报错,而不是报告它无法计算的爆炸半径。`why` 没有这样的限制:它只读取依赖树上已有的父级约束。
### 示例
```
# 当前目录中 module 的人类可读树状图
node dist/cli.js
# 完整的升级详情(各版本的 license + CVEs + 建议)
node dist/cli.js . --upgrades
# 用于 CI 的 JSON,或用于提供给可视化工具
node dist/cli.js . --json > tree.json
node dist/cli.js . --json | node tools/dist/viz.js > report.html
# 交互式 HTML 报告,无需 pipe(与上面的 pipe 使用相同的 renderer)
lichen report . -o report.html
# 门控构建:在遇到拒绝的 license 或 CVSS 超过阈值时失败(exit 2)
node dist/cli.js . --config lichen.config.json ; echo "exit=$?"
# 无网络状态下的快速结构化视图
node dist/cli.js . --offline
```
## 策略 / CI 拦截
每个仓库可以选择是否开启拦截:在扫描的仓库根目录下放置一个 `lichen.config.json`(或使用 `--config` 指定一个)。如果存在,lichen 会对其进行评估,并在**出现任何违规时退出并返回 `2`**,这会导致 CI 步骤失败。如果没有该文件,则表示关闭拦截。
```
{
"licenseDenylist": ["GPL-3.0", "AGPL-3.0"],
"maxOutdated": 5,
"maxCvssSeverity": 7.0,
"minReleaseAgeDays": 14,
"ignoreAdvisories": ["GHSA-xxxx-yyyy-zzzz", "CVE-2024-1234"],
"exceptions": [
{ "id": "CVE-2024-5678", "reason": "patch tracked in JIRA-123", "expires": "2026-09-30" }
]
}
```
| 键 | 效果 |
| --- | --- |
| `licenseDenyList` | 导致拦截失败的 SPDX 表达式(不区分大小写) |
| `maxOutdated` | 允许的、不同的过时包的最大数量 |
| `maxCvssSeverity` | 任何 CVSS 严格高于此值的安全公告都会导致失败(`0` = 只要是有评分的 CVE 就拦截) |
| `minReleaseAgeDays` | 浸泡窗口:低于此天数的锁定版本会发出警告(在 `--strict` 下会失败);同时也会使建议避免指向低于此窗口天数的版本 |
| `ignoreAdvisories` | 永久的、不受管理的静默,匹配安全公告 id 或 CVE 别名 |
| `exceptions` | 受管理且有时限的豁免:`{ id, reason, expires }`。在未过期时,该安全公告被视为“已接受的风险”(会被报告,但绝不会失败);一旦超过 `expires` 时间,它将重新被拦截 |
**为什么需要浸泡窗口?** 采用一个发布版本最危险的时刻就是它刚发布不久。被劫持的维护者和被投毒的发布通常会在发布后的几天内被发现并撤销,因此一个在公开环境中存活了两周的版本,已经接受了生态系统扫描器、研究人员以及其他人的 CI 运行的审查;而一小时前发布的版本则完全没有经过这些。这种模式并非假设:
[chalk/debug npm 劫持事件](https://socket.dev/blog/npm-author-qix-compromised-in-major-supply-chain-attack)(2025 年 9 月;这些基础包每周有 20-30 亿次下载,恶意版本仅存活了数小时)、自我复制的
[Shai-Hulud 蠕虫](https://www.wiz.io/blog/shai-hulud-npm-supply-chain-attack)
(涉及 100 多个 npm 包)以及
[xz-utils 后门](https://www.openwall.com/lists/oss-security/2024/03/29/4)
(CVE-2024-3094,在到达主流稳定发行版之前被捕获)全都发生在这一窗口期内。(依赖更新机器人和包管理器已经开始将同样的控制机制作为“冷却期”或最低发布年龄发布,其中有几个是直接为了应对 Shai-Hulud 蠕虫。)`minReleaseAgeDays` 在两个方向上都进行了应用:过于新鲜的**锁定版本**会发出警告(在 `--strict` 下会失败),而 lichen 自己的升级**建议**也会跳过低于该窗口天数的版本,而不是引导你进入风险区。一个刻意设计的例外是:当针对已知 CVE 的*唯一*修复版本低于该窗口期时,lichen 仍然会推荐它,但会标记其发布时间较短,因为已知的漏洞通常比未知的漏洞风险更大。
没有 CVSS 评分的安全公告会被报告,但默认情况下永远不会被拦截(因此未评分的公告既不会默默失败,也不会在未被察觉的情况下默默通过);`--strict` 会拦截它们。每个不同的 `name@version` 最多只会贡献一次安全公告违规。范围为 `not-imported` 的依赖仅供参考,永远不会被拦截。
安全公告违规是具有可操作性的:它会标记易受攻击的包是**直接**还是**间接**依赖,并提供**该生态系统自身语法**的升级命令。对于间接包,它还会指出引入它的直接依赖,以便你可以升级该依赖,而不是锁定这个包。
```
[advisory] github.com/acme/shared@v1.5.0: ... CVSS 9.1 exceeds maxCvssSeverity=7 -- transitive (via github.com/acme/parent-a); go get github.com/acme/shared@v1.7.0
[advisory] debug@2.6.9: ... CVSS 8.2 exceeds maxCvssSeverity=7 -- transitive (via express); npm install debug@4.4.3
[advisory] serde@1.0.100: ... CVSS 7.5 exceeds maxCvssSeverity=7 -- direct; cargo update -p serde --precise 1.0.219
```
每个生态系统的修复提示:
| 生态系统 | 发出的升级命令 |
| --- | --- |
| Go | `go get @` |
| npm | `npm install @` |
| Cargo | `cargo update -p --precise ` |
| Python | `pip install ==` |
| Ruby | `bundle update ` |
| Maven / Gradle | `mvn versions:use-dep-version -Dincludes= -DdepVersion=` |
| NuGet | `dotnet add package --version ` |
在 Go 中,间接修复命令针对的是易受攻击的模块本身,MVS 会添加间接锁定来强制更新。当不存在可接受的较新版本时,提示会显示为 `-- direct; no fix available`(或者是 `-- transitive (via ...); no fix available`)。
**`exceptions` 对比 `ignoreAdvisories`。** `ignoreAdvisories` 是一个永久的、无条件的静默。`exceptions` 则是更友好的审计追踪豁免:它包含原因和过期时间,在有效期内会在输出中显示为“已接受的风险”,并在过期后自动重新拦截(因此被遗忘的豁免不能永远掩盖某个 CVE)。
**中心化配置分层(组织基础 + 局部覆盖)。** 将 `--config` 指向一个共享的组织策略,并在仓库本地保留一个 `lichen.config.json`,lichen 就会对它们进行分层处理:本地文件只能**收紧**组织基础策略,绝不能削弱它(`maxCvssSeverity` / `maxOutdated` 取两者中较小的值,`licenseDenyList` 取并集,`ignoreAdvisories` 取交集;省略的本地键将继承组织的值)。一个团队可以比组织策略更严格,但不能选择退出。如果只存在一个文件,则按原样使用该文件。
### 在 CI 中使用
`lichen` 是由退出代码驱动的:**`0`** 正常,**`1`** 硬错误(输入错误、缺少工具链),**`2`** 策略违规。在 CI 中,请使用 `--quiet` 运行它(只打印拦截结果,并让退出代码导致步骤失败),如果你想要最严格的姿态,请加上 `--strict`(拦截未评分的安全公告和过于新鲜的锁定版本)。
**Makefile**
```
LICHEN ?= node dist/cli.js
.PHONY: deps-gate
deps-gate: ## fail the build on a dependency-policy violation
$(LICHEN) . --quiet --config lichen.config.json
```
**GitHub Actions** —— 对于那六个基于 lockfile 的生态系统(npm、Cargo、Python、Ruby、NuGet、Gradle),整个作业只需要 Node 和你提交的 lockfile。不需要安装语言工具链,因为 lichen 读取的是 lockfile,而不是调用包管理器:
```
name: deps-gate
on: [push, pull_request]
jobs:
lichen:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20' }
- run: npm i -g @corvalon/lichen
# exit 2 (a policy violation) fails the job; --strict for the strictest gate.
- run: lichen . --quiet --strict --config lichen.config.json
```
**Go 和 Maven** 是两个需要调用外部工具的适配器,因此这些作业需要添加工具链步骤:
```
- uses: actions/setup-go@v5 # Go only: lichen runs `go mod graph`
with: { go-version: stable }
# or, for Maven: lichen runs `mvn dependency:tree -Dverbose`
- uses: actions/setup-java@v4
with: { distribution: temurin, java-version: '21' }
```
通过在仓库中提交一个 `lichen.config.json` 并将 `--config` 指向内部/共享的组织策略,可以在仓库本地策略之上叠加组织级策略:仓库文件只能收紧组织基础策略(参见上文的中心化配置分层)。
**机器可读的拦截报告(`--gate-json`)。** 对于中央摄取器、豁免仪表板或 PR 机器人,可以添加 `--gate-json`,将拦截结果作为 JSON 输出到 stdout(并且不输出其他任何内容,以便顺利进行管道传输)。退出代码保持不变。
```
node dist/cli.js . --quiet --gate-json --config lichen.config.json | jq .
```
```
{
"ok": false,
"violations": [
{ "kind": "advisory", "message": "github.com/acme/shared@v1.5.0: ... -- transitive (via github.com/acme/parent-a); go get github.com/acme/shared@v1.7.0" }
],
"warnings": [],
"acceptedRisks": [
{ "node": "github.com/acme/other@v2.0.0", "id": "GHSA-...", "reason": "patch tracked in JIRA-123", "expires": "2026-12-31" }
]
}
```
当没有硬性违规(退出代码 `0`)时,`ok` 恰好为 `true`。`acceptedRisks` 是**已授予豁免(豁免)的汇总**——每一个处于活跃状态的受管理 `exceptions` 条目及其原因和过期时间,用于审计追踪。
### 物理隔离的 CI
受监管和物理隔离的机构无法将其依赖树发送给 SaaS 扫描器。`lichen` 可以在受限制的机器上**离线运行整个拦截过程,且无需工具链**:它不需要网络,也不需要安装包管理器,只需要一个事实快照(对于 Go 和 Maven,还需要一个捕获的依赖图)。组织策略通过只能收紧的分层机制执行,因此局部覆盖永远无法*削弱*基础策略(参见上文的中心化配置分层)。
**对于那六个基于 lockfile 的生态系统**(npm、Cargo、Python、Ruby、NuGet、Gradle),跨网闸传递的内容只有一样,因为依赖图已经提交到了你的仓库中:
1. **在连接网络的机器上**,对 deps.dev 事实进行快照:
lichen facts . > facts.json # 版本化的 deps.dev 快照
2. **在物理隔离的机器上**,在零网络和零工具链的情况下进行拦截:
lichen . --quiet --strict --offline \
--facts-file facts.json \
--config org-policy.json ; echo "exit=$?"
**对于 Go 和 Maven**,它们的依赖图来自构建工具而不是 lockfile,因此也要在连接网络的机器上捕获依赖图:
1. **在连接网络的构建机器上:**
go mod graph > graph.txt # 模块要求图
go list -m all > list.txt # 已解析/锁定的版本
lichen facts . > facts.json # 版本化的 deps.dev 快照
将 `graph.txt`、`list.txt`、`facts.json` 和你的 `org-policy.json` 复制并跨过网闸。
2. **在物理隔离的机器上**,在零网络和零工具链的情况下进行拦截。依赖图和事实均从文件读取;没有任何数据向外发送:
lichen . --quiet --strict \
--graph-file graph.txt --list-file list.txt \
--facts-file facts.json --offline \
--config org-policy.json ; echo "exit=$?"
离线运行能够重现与相同依赖树的在线运行**完全一致的拦截判定和退出代码**:快照正是拦截器用于推断的确切数据。该快照是一个带版本号的 `{schemaVersion, facts}` 封包,因此由一个 `lichen` 构建捕获的快照可以安全地供另一个构建使用;为了向后兼容,`--facts-file` 读取器也接受传统的裸 map。
**参考 GitHub Action**(以需要捕获依赖图的 Go 为例:连接网络的机器将快照作为 artifact 发布;拦截作业在不使用 `setup-go` 且无网络的情况下使用它)。对于基于 lockfile 的生态系统,请去掉 `setup-go` 和那两行捕获命令,只传递 `facts.json`:
```
name: airgapped-deps-gate
on: [push, pull_request]
jobs:
snapshot: # runs where deps.dev + the Go toolchain are reachable
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5 # Go only; drop for a lockfile ecosystem
with: { go-version: stable }
- uses: actions/setup-node@v4
with: { node-version: '20' }
- run: npm i -g @corvalon/lichen
- run: |
go mod graph > graph.txt # Go only; drop for a lockfile ecosystem
go list -m all > list.txt # Go only; drop for a lockfile ecosystem
lichen facts . > facts.json
- uses: actions/upload-artifact@v4
with:
name: deps-snapshot
path: |
graph.txt
list.txt
facts.json
gate: # runs offline: no toolchain, no deps.dev
needs: snapshot
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20' }
- run: npm i -g @corvalon/lichen
- uses: actions/download-artifact@v4
with: { name: deps-snapshot }
# exit 2 fails the job. No network is touched here.
- run: |
lichen . --quiet --strict \
--graph-file graph.txt --list-file list.txt \
--facts-file facts.json --offline \
--config org-policy.json
```
## 执行升级
`lichen apply` 会将依赖升级到引擎推荐的修复版本(即依赖树中显示的相同的 `安全 > 许可证 > 最高版本` 建议)。它**默认为预演模式**:它会打印出计划和确切的命令,在你添加 `--yes` 之前不会进行任何更改。
```
node dist/cli.js apply # dry-run: every dep with a newer recommendation
node dist/cli.js apply --criticals # only deps with a critical advisory (needs a policy)
node dist/cli.js apply --cve CVE-2024-1234 # only deps carrying that advisory (GHSA or CVE id)
node dist/cli.js apply --recommended --yes # execute the plan
```
该计划始终以被扫描生态系统自身的升级语法打印(参见[策略 / CI 拦截](#policy--ci-gating)下的表格)。`--yes` 会对 Go(先执行 `go get` 然后执行 `go mod tidy`)和 npm(`npm install`)**进行执行**;对于其他六个生态系统,预演计划就是交付物,你需要自己运行打印出来的命令。
预演示例(一个 Go 模块;npm 或 Cargo 项目会打印相同结构,但使用该生态系统的命令):
```
Planned upgrades (2):
github.com/acme/shared v1.5.0 -> v1.7.0 [transitive (adds an indirect require)] -- recommended (relicenses to BUSL-1.1)
github.com/acme/leaf v0.4.0 -> v0.5.0 [transitive (adds an indirect require)] -- recommended upgrade
Commands:
go get github.com/acme/shared@v1.7.0
go get github.com/acme/leaf@v0.5.0
go mod tidy
Dry run (default). Re-run with --yes to execute these commands.
```
每个目标都会被标记为**直接**或**间接**。在 Go 中,间接升级会作为间接 `require`(MVS)添加,并明确显示;当需求数据可达时(在线,或通过 `requirements-file`),Go 的预演还会打印每次升级所引入的 **MVS 强制变更集**(与 `lichen impact` 相同的分析),让你在运行命令前就能看到真正的爆炸半径。`--criticals` 需要一个包含 `maxCvssSeverity` 的策略(来自 `lichen.config.json` 或 `--config`),以便识别关键的安全公告。范围为 `not-imported` 的依赖永远不会成为目标。自我执行仅在 `--yes` 下运行(静态 HTML 报告中的按钮仅复制命令;真正的“一键执行”是延后的 `lichen serve` localhost 模式)。
## 可视化
交互式 HTML 渲染器内置在包中(`src/report.ts`),因此生成报告只需一条命令;`tools/viz.ts` 仍然是 `--json` 契约的纯粹消费者,用于生成静态 Graphviz 格式,并为 HTML 重新导入相同的渲染器,因此每条路径生成的文档都是完全一致的。除了 `npm install` 之外无需安装任何东西——不需要系统级的 Graphviz,也不需要 CDN。输出是自包含的,并且可以离线工作。
```
lichen report . -o report.html # interactive HTML, direct
node dist/cli.js . --json | node tools/dist/viz.js > report.html # same document, via the pipe
node dist/cli.js . --json | node tools/dist/viz.js --format svg > graph.svg
node dist/cli.js . --json | node tools/dist/viz.js --format dot > graph.dot
npm run viz:demo > demo.html # render the bundled fixture
```
默认的 HTML 报告是一个**交互式可折叠树**(纯内联 HTML/CSS/JS):通过延迟展开,让你能够浏览大型深层图的全貌,跟随浏览器/操作系统的**浅色/深色**主题,并提供**名称 + 严重性过滤器**(关键 / 安全公告 / 过时 / 许可证变更)。每个节点都显示版本、父级约束、许可证、安全公告、语言标签和一个 **`↑ recommend`** 徽章;“N newer” 标签会展开一个**堆叠的逐版本列表**(发布日期、许可证以及每个版本包含的 CVE)。CVE 链接到 **OSV**;包名链接到 **deps.dev**。颜色始终与文本标签配对显示(符合 ADA 无障碍标准)。`--format svg` / `dot` 通过 `@viz-js/viz`(编译为 WebAssembly 的 Graphviz)渲染静态 Graphviz 图像。
## VS Code 扩展
从 [Releases 页面](https://github.com/Corvalon/lichen/releases/latest)下载最新的 `lichen-vscode-*.vsix` 并运行 `code --install-extension lichen-vscode-.vsix`(或者在扩展视图中点击 `...` 菜单,选择 **Install from VSIX**)进行安装。如果是为了开发:
```
cd vscode-extension && npm install && npm run build # build the engine first
```
……然后按 F5 启动扩展开发宿主。“Lichen” 视图拥有自己的活动栏图标,涵盖了与 CLI 相同的生态系统,并能从工作区的构建文件中自动检测。该树状图与报告相呼应:每个节点的描述显示版本、许可证、安全公告数量和推荐的升级;悬停提示框增加了发布日期、带有权衡取舍的建议、指向 OSV 的 CVE 链接、deps.dev 链接,并且(在开启 `lichen.fetchUpgrades` 时)包含逐版本的升级路径。扫描会在项目加载时在后台开始,在任何生态系统的清单/lockfile 发生变化时重新运行,并针对可配置的发现维度发出警告(`lichen.warnOn`:critical / advisory / outdated / license-change / recommended,并在图标上显示徽章)。你可以从视图中在浏览器中打开完整的 HTML 报告,或者应用推荐的升级:Go 和 npm 会直接执行(`go get` + `go mod tidy` / `npm install`,带有确认和许可证变更同意机制);其他生态系统则显示确切的命令并支持一键复制。设置包括:`lichen.offline`、`lichen.fetchUpgrades`、`lichen.prefetch`、`lichen.warnOn`、`lichen.critical`、`lichen.minReleaseAgeDays`。
## 工作原理
一个独立的 TypeScript **引擎**是唯一的真相来源,被所有的前端(CLI、VS Code 扩展、可视化工具)复用:
```
ecosystem adapter ──RawGraph──▶ engine ──DepTree──▶ front-ends (CLI / extension / viz)
(src/ecosystems/*) │
8 adapters, one shape └── enrich ──▶ deps.dev (license/versions/advisories)
└─▶ go.dev (language version, Go only)
```
添加一个生态系统意味着在 `src/ecosystems/` 下编写**一个适配器**,它返回一个通用的 `RawGraph`,加上(对于非 semver 生态系统)在 `src/version-schemes.ts` 中添加一个比较器;引擎和前端无需更改。目前已发布八个适配器:**Go**、**npm**、**Cargo** (`Cargo.lock`)、**Python** (`uv.lock` / `poetry.lock`)、**Ruby** (`Gemfile.lock`)、**NuGet** (`packages.lock.json`)、**Maven** (`mvn dependency:tree -Dverbose`,其详细输出会暴露出受冲突调节的父级约束) 和 **Gradle** (`gradle.lockfile`)。`Ecosystem` 联合类型的值直接映射到 deps.dev 的系统名称,因此每个生态系统的富集过程都是零配置的。
## 路线图
正在进行的开发和开放的设计决策记录在项目看板上(指针请参见 [`TODO.md`](TODO.md))。已发布的功能包括:范围分类、经过加固的 CI 拦截器(`--quiet`、带有过期时间的受管豁免、浸泡规则、可操作的拦截消息)、`lichen apply`(默认预演模式)、`lichen report`(相同的渲染器驱动 VS Code 扩展的“打开 HTML 报告”),以及八个生态系统适配器,每个都有真实的验证语料库(Gradle 是为 lockfile 格式编写的;其余的由它们各自的工具链生成)。开放的重点:
- **GitHub 可渲染的报告 + 健康评分**(Markdown + Mermaid;每个类别的评分,其中总体评分 = 最差的那个类别)。
- **在扩展中对非 Go/npm 的生态系统执行升级**(目前它们的升级命令仅提供一键复制,而不是直接运行)。
- 发布 `lichen` 二进制文件;增加更多生态系统。
## 贡献
```
npm install && npm run build && npm test # node:test, hermetic (no toolchain/network)
```
引擎具有可注入的接口(`CommandRunner`、`FileReader`、`runtimeResolver`、`FactsProvider`),因此整个套件,包括所有八个适配器,都可以在零工具链和零网络的情况下运行。保持前端只负责展示;将新逻辑放入引擎中。架构约定请参见 [`CLAUDE.md`](CLAUDE.md)。
## 许可证
**MIT**(参见 [`LICENSE`](LICENSE))。该引擎**没有第三方运行时依赖**,并且所有的构建/测试工具(TypeScript、`@types/*`、`@viz-js/viz`)都是宽松许可证,因此没有任何东西会限制你的选择。宽松的核心意图就是保持宽松;未来任何商业/企业功能(中央控制平面)都将是独立的附加组件,而不是对核心的重新许可。
标签:MITM代理, 依赖管理, 多语言, 暗色界面, 离线工具, 自动化攻击