SentinelloSentinello

为那些你不再盯着看的依赖,提供一套早期预警系统。

在 AI 时代,你发布的项目多过你能维护的数量。Sentinello 会盯住其中的每一个,揭示其 Node.js 依赖中已知的 CVE —— 让被遗忘的项目永远不会演变成事故。

一条命令即可运行:

docker run -d \
  --name sentinello \
  -p 127.0.0.1:3870:3000 \
  --stop-timeout 60 \
  -v sentinello-data:/app/data \
  -v sentinello-nvm:/home/sentinello/.nvm \
  -v ~/Developer:/roots/personal:ro \
  ghcr.io/walkofcode/sentinello:latest

运行于 linux/amd64 和 arm64

无需账号。没有 SaaS。没有遥测。一个 Docker 镜像和一个 SQLite 文件 —— 你的代码和结果绝不离开你的机器。

也可以完全不用门户

同样的扫描器还提供 CLI。无需安装、无需账号、无需数据库——运行后输出一份代理可直接处理的公告,然后退出。

npx sentinello

通过管道传递时,stdout 只承载 markdown,因此公告能完整送达代理:

npx sentinello | claude -p "$(cat -)"

遍历文件夹

指向一个目录,它会找出其下的每个项目,并在各项目根目录处停止,因此 monorepo 只算一个而不是五十个。会遵循 .gitignore 与 .sentinelloignore。

核对三个来源

从 lockfile 解析出实际安装的精确版本,离线比对本地公告缓存。不会为每个项目发起网络请求,也不会上传你的任何代码。

写出公告

一份带日期的 markdown 文件,包含发现的问题以及附带的修复提示——先分诊再动手,优先升级父包而非使用 override,并在 lockfile 中验证修复。

与 npm audit 有何不同

它并不取代 npm audit:它会运行 npm audit,然后补上两个 npm audit 看不到的公告来源,并抑制其中重复的部分。

npm auditnpx sentinello
范围你当前所在的那一个项目。一个文件夹下的所有项目,一次执行、一份报告。
公告来源你所用注册表的公告源。在此基础上再加 OSV 与 GitLab gemnasium。重复项会被抑制,因此每增加一个来源只会带来新增的发现。
恶意包不涵盖。OSV 的 MAL- 记录会标记随恶意代码一同发布的包——仿冒名、安装脚本载荷——并与具体被投毒的版本进行比对。
输出一张表格,或需要你自行解读的 JSON。一份附带修复提示的 markdown 公告,可直接交给代理。
比对方式每个项目都要调用注册表。针对本地缓存离线比对。首次运行会先征询再下载,之后几乎不传输任何数据。

这些额外来源并非摆设。在 Sentinello 自己的仓库上,npm audit 与 OSV 各报告三条发现且完全一致,而唯一的 critical 发现来自 gemnasium——另外两个来源都没有收录它。

功能

尽在一个自托管门户 —— 没有外部服务,数据不会离开你的网络。

单一分诊队列

在一个地方查看并分诊整个组合的 CVE —— 而不是散落在十几个检出目录里的 npm audit。

按项目或库浏览

深入任意仓库,查看它的发现、修复版本和历史 —— 或切换到某个易受攻击的包,看它影响的每一个项目,并一次性在所有项目中将其静音。

彼此印证的发现

当多个来源报告同一个漏洞时,它仍然是一条发现 —— 并带上哪些来源与之一致,按其中任一给出的最严重等级来评定。

持续扫描

后台工作进程按计划重新扫描,新的公告会自动出现,无需你记得去检查。

多种来源

不止 npm audit:同时与 OSV 和 GitLab gemnasium 匹配,获得更广的 CVE 覆盖并检测已知恶意软件包。

通知与 Webhook

通过 Slack、Telegram 或一个普通 Webhook 接收失败与发现的告警 —— 可按 root 或项目划分范围,并使用你选择的语言。JSON 或纯文本负载,可直接交给自动修复代理。

MCP 服务器

连接 Claude Desktop、Cursor 等 MCP 客户端,无需离开聊天即可查询发现、项目和库,并触发扫描。

公告导出

将项目或库的发现导出为 Markdown,并附带可为你的团队或 LLM 自定义的修复提示。

一个镜像,一个文件

一个 Docker 镜像和一个 SQLite 文件。没有数据库服务器,没有消息队列,没有云端依赖。

自动注册的 Roots

挂载到 /roots 下的任何内容都会在启动时被注册和扫描 —— 目录名会成为它的标签。

按项目区分的 Node

遵循每个项目的 .nvmrc,将其固定的 Node 版本安装并缓存一次。

10 种语言

门户界面、扫描原因代码和状态均已本地化为 10 种语言。

截图

实际效果一览 —— 门户正在扫描几个演示项目。点击任意截图即可放大查看。

对比

Sentinello 不是更重的 Dependency-Track,也不是更便宜的 Snyk。它占据另一个定位:没人接入流水线的那一长串项目。

SentinelloDependency-TrackSnykDependabot
零配置 — 指向一个文件夹即可~~
无需 SBOM / CI 步骤
扫描真实解析的锁文件~
恶意软件包检测
自托管,无 SaaS
单一镜像 + SQLite
AI 原生(MCP + 导出)~
多语言(Python、Go……)
企业策略 / VEX~~

Dependency-Track 只能看到有人用 SBOM 流水线接入的项目。Sentinello 找出你遗忘的那些。它们在企业策略上更强 —— 如果你已在成熟流水线中运行它们,继续用。Sentinello 面向你那部分无人看管的其余项目组合。

我们为何打造它

在 AI 时代,你发布的多过你能维护的。

如今,一个独立开发者一年就会启动、交付、然后放下十几个项目 —— 营销站点、客户仪表盘、悄悄上了生产的业余项目。要让它们保持安全,过去意味着逐个 SSH 进每个检出目录手动跑 npm audit,或者在某个 Next.js CVE 爆出几天后才从新闻标题里得知。没人能对十几个仓库坚持这么做,所以这事根本就没人做。

只需一个被遗忘、带有严重远程代码执行漏洞的依赖就足够了。那个你不再盯着看的最简单的站点,会成为入侵的入口。

“为什么不直接用 Snyk 或 Dependabot?”那些工具活在你接好的 CI 流水线里 —— 而这条长尾从来就没有流水线。Sentinello 正是其余一切的早期预警系统:把它指向一个文件夹,它就会盯住你遗忘的每一个项目,在每个新 CVE 演变成事故之前,把它汇集到一个队列里浮现出来。

工作原理

三个步骤。无需在你的项目中安装代理,也无需创建账号。

把它指向你的代码

将你的仓库挂载到 /roots,或在“设置 → Roots”中添加。每个目录都会在启动时被自动注册和发现。

持续扫描

后台工作进程按计划将你的依赖与已知 CVE 比对,并在需要时安装每个项目所固定的 Node 版本。

在一个队列中分诊

所有项目的每一条结果都汇入一个可按严重程度筛选的队列 —— 还可选择向 Slack、Telegram 或 Webhook 发送告警。

适合谁

Sentinello 适合所有在生产环境里拥有的东西多过自己能盯住的人 —— 独立开发者、小团队、忙于客户项目的工作室。

  • 你发布的业余项目和客户站点,在上线后很久仍需保持安全。
  • 你想要覆盖整个组合的视图,而不必为每个仓库接入 CI。
  • 比起把代码清单交给 SaaS,你更愿意自托管。

如果你是一家大型机构,已经在成熟的流水线中接入了 Snyk 或 Dependabot,那就继续用它们 —— Sentinello 并不想取代企业级 SCA。它面向的是你那部分无人盯防的代码组合。它是开源且采用 MIT 许可证的,因此你可以确切地读到它在做什么。

发行说明

Sentinello 持续更新——以下是每个版本带来的内容。

一个让单个来源代表整个项目发言的仪表板

v3.5.0 · 2026年8月20日
  • 仪表板的<strong>状态</strong>列只报告一个来源,其余被丢弃。每次扫描会为每个来源写入一行,而它们相隔仅几毫秒完成,因此该列显示的是最后完成的那个——实际上总是 OSV。于是每个项目都显示“OSV 数据库尚未下载”,而 npm audit 明明已顺利扫描并发现了真实漏洞。现在“状态”会为每个无法给出答复的来源各显示一个标记并注明来源名称;当所有已启用的来源都正常时,则什么也不显示。扫描历史出于同样的原因新增了<strong>来源</strong>列:一次扫描会写入三行看起来完全相同的记录,无从分辨。
  • 设置 → 来源在缓存重建期间仍声称它是最新的。门户读取的状态只在同步结束时写入,因此在长达数分钟的重建过程中,它一直显示重建之前的数量——而与此同时每次扫描都正确地拒绝了这个删除到一半的缓存。它也从未携带扫描器所要求的规范化版本号,所以仅仅提升版本号就能在完全没有重建的情况下产生同样的虚假说法。该行现在会显示<strong>正在重建…</strong>并将之前的数量淡化,或者在缓存不可用且当前无人处理时显示<strong>等待重建</strong>。
  • 缓存下载完成后,每个项目仍停留在缓存缺失期间得到的结论上。没有任何机制重新扫描,因此 <code>osv_db_not_seeded</code> 会一直留在那里,直到下一次计划扫描,或者直到你注意到并手动点击扫描。现在只要缓存重新变得可用,Sentinello 就会立即排入一次全量重新扫描。增量更新则不会排入任何任务。 升级同样适用:如果本次发布落到一个项目仍带着缓存完成之前那个结论的实例上,worker 会在首次启动时发现这一矛盾并替你清除——无需记着手动运行扫描。

把修复版本标记为有漏洞的公告

v3.4.0 · 2026年8月18日
  • gemnasium 有少量公告在比较运算符与版本号之间写了一个空格 — 是 <code>&lt; 0.5.2</code> 而不是 <code>&lt;0.5.2</code> — 解析器把这一对读成了两个独立的词元。落单的 <code>&lt;</code> 得到一个空边界,而被独自留下的版本号则被当作精确锁定的版本缓存起来。于是 <code>fresh</code> 的公告把 0.5.2 报告为有漏洞,而 0.5.2 恰恰是修复该问题的那个版本;并且报告为无可用修复,因为锁定的版本根本不带任何升级目标。共有 19 条公告是这样书写的,每一条都解析错误:15 条完全丢失了版本范围,7 条锁定了记录自身列为修复版本的版本,<code>pg</code> 一次锁定了十一个。
  • Sentinello 不再自行解释 npm 的版本范围语法,而是先把每个范围交给 npm 自己的实现处理,这一举措同时终结了一整类误读。对 npm 而言,<code>&lt;=3.3</code> 表示“直到 3.3 系列的末尾”——按字面理解会停在 3.3.0,从而漏掉 3.3.1,而这正是 <code>converse.js</code> 的一条真实公告;<code>=103</code> 表示整个 103 系列而非单独一个点,<code>binaryen</code> 有八条公告是这样写的。插入符、波浪号、<code>1.x</code> 和连字符范围现在也能读取,而此前用其中任何一种写成的记录都会被悄无声息地丢弃。在 gemnasium 为 npm 发布的全部 4,696 个不同版本范围上核对:由我们自己解释时与 npm 有 9 处分歧,交由 npm 处理则没有任何分歧。这只影响 npm——Python 的 <code>==1.0</code> 是精确版本而非通配符,把 npm 的规则套用过去会凭空造出检出结果。
  • npm 的解读在安装软件包时正确、用于公告时却不正确的三处,被保留了下来不予采纳。完全不指名版本的范围 — <code>*</code>、<code>x</code>、空字段 — 在 npm 安装时表示“任意版本”,在公告里却与“没人填写这一项”无法区分,因此予以拒绝,而不是把它变成针对已发布全部版本的检出结果。末尾多余的 <code>||</code> 也不再能把它前面的范围扩大到全部。并且 <code>&gt;0</code> 仍然表示所有版本,因为 <code>pandora-doomsday</code> 就是这样声明其恶意软件包的,而按 npm 的解读会判定整个 0.x 无害。另外,若公告声明的修复版本位于其自身受影响范围的起点或起点之下,将不再用它替换边界——那样会产生一个不匹配任何版本的区间,也就是一条悄悄停止报告的检出结果——而且丢弃这类区间的规则现在与 OSV 数据源共用,不再每个数据源各写一份,正是各写一份才导致两者最初产生了分歧。
  • 一直没能发现上述所有问题的测试,现在会生成自己的输入,而不再逐条罗列。gemnasium 为 npm 发布的每一个不同的版本范围,都会在每次构建时与 npm 自身的实现进行比对,同时还会检查范围语法的完整组合,以及 OSV 范围事件的所有排列顺序。针对上一个版本运行时,这次全量比对会在 28 个范围上失败,而被它取代的那十个手写示例则全部通过。从现在起,上游若发明出新的写法,构建会在其首次被导入时失败,而不是等到有人注意到它产生的检出结果。
  • 支撑这些结果的每一行代码现在都由测试套件执行——语句、分支、函数和行均达到 100%,整个仓库没有任何豁免。这直接回应了前几个版本的缺陷是如何漏出去的:每一个都是无人执行的分支,什么也没做却报告成功。在补上最后 109 处的过程中,发现其中若干被记为不可达的安全检查,实际上只是没有测试;还发现一条覆盖率规则自其所指的文件被移动之后,数月来什么也没有检查。这条规则连同周围 500 行例外一并被一条规则取代,因此未经测试的代码现在会让构建失败,而不是拉低平均值。
  • 最后的防御缺口也已补上:无法读取的 OSV 上限会让有效区间保持开放,回退边界会在应用后验证,而像 <code>&lt;1.2.3-0</code> 这样的显式 npm 预发布边界会保留其精确含义。

声称每个版本都存在漏洞的公告

v3.3.2 · 2026年8月17日
  • 受影响范围没有上界的公告会永远匹配所有版本——这种结果无论怎样升级都无法消除,在页面上也无法与真正尚未修复的漏洞区分开。有两条 <code>xlsx</code> 公告正是如此,把已完全修复的 0.20.3 报告为高危且无可用修复,同时波及 11 个项目;而 <code>npm audit</code> 对这两条的报告是正确的,分别在 0.19.3 和 0.20.2 中修复。根因在上游,而且是有意为之:如果某个修复版本没有以该包名发布到registry,GitHub 就不会写出这个版本——SheetJS 的 0.19.3 及之后版本只通过自家 CDN 发布——于是它把范围写成“从 0 开始的全部”,并把真正的边界记录在另一个字段里,而 Sentinello 从未读取该字段。现在它会读取了。受影响的 npm 公告共 15 条,其中包括 <code>babel-traverse</code> 和 <code>sandbox</code>。那 480 条确实没有修复的公告仍然照实显示,并且已经写明自身边界的记录绝不会被覆盖
  • gemnasium 公告若一边列出修复版本、一边让范围保持开放,现在会以其中最高的版本作为上界。没有上界的范围等于宣称将来的每个版本也有漏洞,而任何写明了修复版本的记录都不可能是这个意思
  • gemnasium 用 PEP 440 交集来书写 Python 的版本范围——<code>&gt;=5.0,&lt;5.8</code>——而解析器只按空格切分标记,于是整段变成一个标记:下界无法解析,上界则完全丢失。这样的范围匹配不到任何东西,而匹配不到任何东西就意味着什么都不会报告。缓存的 7,159 条 PyPI 记录中有 2,830 条处于这种状态。这正是 3.3.0 中撤下 Python、Go 和 Rust 的三个原因之一;另外两个仍未解决,因此它们继续保持撤下状态

与 3.3.0 相同的版本,但 Docker 镜像可以构建

v3.3.1 · 2026年8月15日
  • 3.3.0 已发布到 npm 和 GitHub,但它的容器镜像构建失败,因此发布时 GHCR 和 Docker Hub 上都没有 3.3.0。Dockerfile 会逐个列出它要安装的工作区包,而其中负责版本比较的那个包一直没有被列进去。在本次发布之前,镜像里没有任何东西 import 它,所以这个遗漏从未产生过影响。此后我们已用 3.3.0 自身的应用代码重新发布了修正后的 3.3.0 镜像,因此 <code>docker pull sentinello:3.3.0</code> 又可以正常使用了。3.3.1 则是把同一处修正带入了源码树。如果你使用 CLI,3.3.0 本来就是正确的。

只保留真正可用的生态系统 —— 以及值得信赖的通知

v3.3.0 · 2026年8月15日
  • 通知可能会空着送达。一位运维收到一条 Telegram 消息,声称某个项目存在漏洞,却一条也没有列出。派发以项目为范围,运行却是按扫描器进行的:npm audit 那一轮拿到了两个待发的 OSV 事件,一个都没匹配上,仍然在空列表之上渲染了标题。随后它把两个事件都标记为已送达 —— 于是那两条从未被提及的发现被记录为已通知,而已送达的事件不会再被回看。现在只有能够被描述的事件才会派发;无法描述的会保持待处理,并在下次扫描时重新考虑
  • 一条发现会以任一来源给出的最严重等级来上报,而这种升级此前能到达仪表盘、项目汇总和 CLI 的 <code>--fail-on</code> 闸门 —— 唯独到不了通知阈值。通知在互证之前运行,因此事件被打上了幸存来源自己的等级,之后再没有被改写。在一台真实实例上,135 条未解决的发现所带的事件严重程度低于其真实值,<strong>其中 41 条把 critical 记录成了 low、high 或 moderate</strong>。一个筛选到 critical 与 high 的通知目标,将永远不会为其中任何一条被呼叫
  • 计划扫描在午夜停下而不是继续。“从 07:00 起每 3 小时”会在 07、10、13、16、19、22 点运行,然后一直到次日 07:00 才再次运行 —— 每天六次而不是八次,每晚留下九小时的盲区 —— 与此同时设置页仍然显示你选择的间隔。“从 20:00 起每 6 小时”则每天只跑一次。现在时间点会跨过日界继续
  • <strong>Python、Go 和 Rust 已被撤下。</strong> 这不是对粗糙之处的谨慎:它们的缺陷会以“干净”的形式呈现。修复版本推导与版本排序只支持 semver,因此一条关于 Django 的 OSV 公告会对已安装的 4.2 建议“升级到 3.2.23”;OSV 的 PyPI 包名未按 PEP 503 规范化,因而永远无法与解析器的名称对上;gemnasium 的范围解析器读不了 PEP 440 的逗号交集。一个出于错误原因回答“没有漏洞”的来源,比根本不提供更糟,所以它们已从产品界面上完全消失 —— 没有开关,不做发现,不做下载。<strong>你已经收集到的发现仍然可见、可静默;不会删除任何内容。</strong> npm 生态系统现在显示为 <strong>Node.js</strong>,指的是真正被扫描的包生态系统,而不是一门语言
  • <strong>设置 → 来源</strong> 也围绕这一点重建。以前每个来源都在自己的开关旁重复自己的说明,每个带缓存的来源还各自带着五行状态面板和一个整尺寸的“立即刷新”按钮 —— 尽管无论出现多少次,该按钮每个来源只会入队一个共享信号。现在开关只说明来源是否启用;每个来源补充什么、从哪里下载什么、何时运行,都移到了下方的一张参考表里,同步状态也收拢成一行
  • npm 自己的锁文件声明可从生产环境到达的包,却被降级为仅开发。锁文件中没有 <code>dev: true</code>,正是 npm 在断言该包确实可从生产环境到达 —— 这比根清单能作出的断言更强 —— 但只要该名称同时出现在 <code>devDependencies</code> 中,解析器就会覆盖它,而那恰恰是 npm 正确的时候。在 130 个真实项目上测得:97 个项目中有 142 个包被降级 —— lodash、semver、postcss、tailwindcss、@babel/runtime —— 在一台实例上让七条未解决的发现从“仅生产”筛选中消失
  • 任何被固定到类似 <code>0.0.0-20180523222229-09b5706aa936</code> 版本的包,<strong>不会匹配任何公告</strong>,连开放区间的都不会,而扫描却报告 ok 且零发现。公告下界 <code>introduced: 0</code> 被当作发行版 0.0.0 来比较,而在 semver 中预发布版本排在其发行版之下。真实缓存里 19,085 个可比较区间中有 330 个被存成了任何版本都无法满足的区间
  • 一次被中断的 OSV 同步可能永久删除一条公告。增量路径先删除公告的行,再在 try/catch 中获取替代内容,因此任何超时、5xx 或关机都会把它整条抹掉 —— 之后游标仍照常前进,把该 id 永久地留在身后。这种丢失是无声的,会一直持续到下一次完整重新播种,而且发生在一次报告成功的同步中。<code>npx sentinello</code> 的缓存也在每次不稳定的运行中同样被侵蚀。现在两者都先获取、后替换
  • CLI 的 <code>--dep-type dev</code> 意思是“只要能从 dev 到达”,而门户的意思是“<em>只能</em>从 dev 到达”,因此一个从两边都能到达的包会出现在一个视图里而不出现在另一个里 —— 在一台实例上,两种解读之间相差 177 条未解决的发现。CLI 现在采用门户的规则,并遵守 OSV 的 <code>withdrawn</code> 字段,而它此前在结构上根本无法读取该字段:真实 npm 缓存中有 585 行带有这个值,而它们全都被当作有效发现上报
  • 当 gemnasium 公告表示漏洞从某个版本<em>之后</em>开始时——写作 <code>&gt;1.2.8</code> 而非 <code>&gt;=1.2.8</code>——它却被当作边界版本本身也受影响。2021 年 <code>rc</code> 包被劫持的公告正是这样写的,而 1.2.8 是它最后一个干净的版本:恰恰是该公告自己的修复说明让你保持使用的那个版本。于是每个安装了 <code>rc</code> 的项目都会看到一条严重的恶意代码告警,且没有可用修复,针对的却是一个从未被污染的版本。现在边界会完全按公告的表述保留,这类误报的严重告警随之消失
  • 同样的取整在相反方向上也在发生,并且掩盖了真实的发现。以 <code>&lt;=2.0.0</code> 为界的公告被存成“低于 2.0.0”,因此它表述得最明确的 2.0.0 反而未被报告;而只指明一个受影响版本的公告会塌缩成空区间并被整条丢弃。预计会出现少量新发现——它们一直都在,只是此前不可见
  • 用 Sentinello 未实现的语法书写的范围——<code>^1.0.0</code>、<code>~1.0.0</code>——过去会被当作对字面文本的精确版本锁定,只要还留在缓存里就永远无法匹配任何东西:看起来有效、却根本不可能触发的公告。现在这类记录会被直接拒绝,而不是以无法工作的形式保存;上界没有干净修复版本的公告,也终于能给出升级建议
  • 那个在 webhook URL 或机器人令牌进入日志行之前对其进行掩码的辅助函数,会把短的原样打印出来。它会拒绝六个字符及以下的值,却又保留八个字符的头部和四个字符的尾部,并且没有任何地方检查这两半是否会相接 —— 于是每一个 7 到 12 个字符的密钥都被完整地返回了。现在它至少隐藏八个字符,否则就把整个值完全屏蔽

结果现在会显示哪些来源意见一致,且已撤回的公告不再被报告

v3.2.0 · 2026年8月15日
  • 当多个公告数据库报告同一个漏洞时,Sentinello 一直只保留一条结果——因为三个数据库都知道就把同一个缺陷报告三次,那是噪音。但此前它会丢弃被合并来源的全部信息,于是一个由 npm audit、OSV 和 GitLab gemnasium 各自独立确认的漏洞,看上去与只有一个数据库听说过的漏洞毫无二致。在真实实例上这占全部结果的三分之二。现在每条结果都会带上其他报告过它的来源,其标记与保留下来的那条并列显示
  • 一条结果会以任一来源给出的最高严重级别来报告。各数据库的评级确实存在分歧——gemnasium 依据 CVSS 向量计算,而 npm audit 采用 GitHub 的分级——对扫描器而言,值得据以行动的是更谨慎的那个读数。这并非只是外观改动:被提升的结果会在仪表盘、项目汇总、CLI 的 <code>--fail-on</code> 门禁以及通知阈值中改变所属级别。升级后的首次扫描可能出现计数变动;并没有检测到新的问题,只是同样的结果被更谨慎地评级了
  • 在来源存在分歧时,结果会在严重级别旁显示一个控件,打开后可查看每个来源的实际说法——它自己的公告编号与自己的评级。它仅在存在需要解释的分歧时出现,因此各来源评级一致的结果仍保持简洁
  • OSV 用一个专门的字段记录撤回,GitHub 则在 <code>npm audit</code> 看到之前就移除已撤回的公告,但 GitLab gemnasium 的模式中没有这样的字段。它通过就地重写记录来撤回公告——标题变为“False Positive”“Withdrawn Advisory: …”或“Duplicate Advisory: …”,而此前列出的版本原封不动地留在那里。Sentinello 读取了这些版本,报告了 GitLab 已明确收回的结果:涉及 JavaScript、Python、Go 和 Rust 的 383 条记录,其中一条以“False Positive”为标题报告了 <code>express</code>。现在这些记录全部被丢弃,这同时消除了一整类重复结果——383 条中有 278 条正是因与其他公告重复而被撤回的
  • 该判断以精确匹配撤回标记为准,而不是在文本中随处搜索这些词语,因此真正*讨论*误报的公告仍会被报告:标题为“Cosign’s verify-blob-attestation reports false positive when payload parsing fails”的 Cosign CVE-2026-39395 不受影响
  • 升级后首次同步时,gemnasium 缓存会自行重建,届时已撤回的公告随之消失。你无需做任何事:它会在每日同步中完成,也可从“设置 → 来源 → 刷新”立即执行

公告的版本范围在两个方向上都正确了

v3.1.1 · 2026年8月14日
  • 部分 GitLab gemnasium 公告完全不含机器可读的版本范围——JavaScript 的 10,777 条中有 698 条。Sentinello 此前通过假定“低于所列第一个修复版本的全部版本都受影响”来填补这个空缺,但该列表是无序的,且每个发布分支各有一个修复版本,因此这个猜测经常落在错误的分支上。protobufjs 7.6.5 被报告为严重的远程代码执行,尽管该分支已在 7.5.5 中修复;另有三条公告分别声称低于 8.0.5 的所有 vite 版本都存在漏洞。Sentinello 不再臆造范围:它会从以另一标识符发布的同一条公告、公告自身的描述,或——仅在你启用了 OSV 时——从你机器上已有的 OSV 副本中恢复真实范围;若以上都无法确定,则丢弃该记录而不做猜测。预计会有一些严重级别的结果消失
  • OSV 将一条在多个发布分支上修复的公告描述为每个分支一条独立条目,而 Sentinello 只保留第一条、丢弃其余——仅 JavaScript 就丢失了 1,927 个存在漏洞的版本范围,每一个都是它再也看不到的真实漏洞。minimatch 的公告涵盖八个分支,却只有一个幸存,因此已安装的 minimatch 3.0.4 或 9.0.0 未被报告;next 和 ua-parser-js 也以同样方式丢失了分支。现在所有分支都会保留。预计会出现一些新的结果——这些漏洞一直都在,只是此前不可见
  • 升级后首次同步时,两个公告缓存都会自行重建,因为它们保存的范围是旧代码生成的。你无需做任何事:它会在每日同步中完成;若不想等待,也可从“设置 → 来源 → 刷新”立即执行

已静默的发现项彻底让位,数据库也不再无限膨胀

v3.1.0 · 2026年8月13日
  • 已静默的发现项是你早就做出的决定,因此它现在会完全从项目页面移除,而不是灰着留在那里。这不只是为了整洁:页面上的每一个数字——标题计数、两个标签页的角标、分页、各依赖库的合计、导出按钮——都基于同一批数据计算,页面终于与仪表盘、MCP 工具以及通告导出保持一致(后三者本来就已排除静默项)。“显示已静默项”开关可以随时把它们调回来,即使某个项目的发现项*全部*被静默也一样
  • 在对话框中输入不会再打完一个字符就丢失焦点。这个缺陷让静默对话框的“原因”字段几乎无法填写——而它是必填项,也是几个月后追溯一次静默是否合理的唯一依据。同一个对话框也不再继承打开它的表格行的对齐方式,这正是静默某个发现项时对话框内容右对齐、而静默整个项目时却正常的原因
  • 扫描历史不再无限增长。此前从未有任何机制按时间删除扫描记录,因此只要项目还在磁盘上,就会按来源、按每轮扫描无休止地累积记录——某个真实环境在不到三个月内达到了 2.2 GB。“设置 → 高级”中新增了保留期,默认 90 天,工作进程每小时清理超期记录,同时始终保留每个项目最近的 100 次扫描。发现项、静默设置和通知历史绝不会被删除,受影响的只有扫描日志。按 90 天的默认值,升级后的实例首次运行不会删除任何内容:只有当历史确实超过该期限,或你自己调低了它,清理才会开始
  • 这些增长绝大部分来自 `npm audit` 的原始输出:每次成功扫描都被完整保存,却没有任何地方读取它——占了那个环境数据库的 98.7%。现在扫描只记录一份简短摘要,单条记录从约 79 KB 降到 100 字节左右,并设有硬性上限,任何扫描器都无法重演这一幕
  • 在 MCP 方面,`get_dashboard_summary` 现在会说明:被你静默的项目会从它的合计中剔除,而 `list_projects` 仍会返回该项目。两者统计的本就是不同的范围,此前对比二者的智能体会把这当成缺陷。`list_scans` 也不再返回每次扫描的扫描器原始输出——单次响应曾可能达到约 16 MB

gemnasium 下载恢复正常,CLI 也会正常退出

v3.0.1 · 2026年8月4日
  • 在 3.0.0 中,所有用户的 GitLab gemnasium 下载都会以 `HTTP 406` 失败。Node 内置的 fetch 会附加程序无法移除的 `Sec-Fetch-Mode: cors` 请求头,而 GitLab 会拒绝任何带该请求头的仓库归档请求 — 所以这与你的网络、IP 或重试次数都无关。下载现已改用普通的 HTTPS 请求,可以成功
  • 归档现在按提交 ID 而不是分支名获取,因此从同一上游提交更新的所有人共享同一份缓存副本,而不必各自让 GitLab 生成一个 60 MB 的归档。原本接近七分钟的首次下载现在几秒即可完成
  • CLI 过去会完成全部工作 — 写出报告、打印摘要 — 然后再也不把终端交还。下载所用的连接仍在后台保持打开,使进程无法退出;现在读完归档后就会立即关闭
  • 拒绝下载的数据源不再等待三分钟才报告。它会在几秒内报告,并在终端中询问是否重试 — 且只重试真正失败的那个数据源
  • `--fail-on` 在两个方向上都变得诚实。当某个公告数据源无法访问时,它会拒绝该次运行,而不是报告一次从未真正执行的干净扫描;同时,对于你自己用 `SENTINELLO_OSV_FEED_URL=off` 或 `SENTINELLO_GEMNASIUM_FEED_URL=off` 关闭且从未下载过的数据源,它不再让运行失败

Sentinello 现在完全不用门户也能运行

v3.0.0 · 2026年8月3日
  • 扫描器以 CLI 形式发布到 npm。`npx sentinello` 会遍历一个文件夹,找出其下的每个项目,对照 npm audit、OSV 与 GitLab gemnasium 进行核查,并写出一份附带修复提示的 markdown 公告——无需安装、无需账号、无需数据库,你的代码也不会离开本机
  • 通过管道传递时,stdout 上只有公告,因此 `npx sentinello | claude -p "$(cat -)"` 能把一份完整的工作清单交给代理,而不会有任何东西破坏该文档
  • 首次运行不会再因下载被拒而丢掉 gemnasium 来源。GitLab 会一次拒绝其归档一到两分钟,而旧的重试十三秒就放弃了;现在 CLI 会等它过去、说明自己为何在等待,并在默认的三分钟不合适时接受 `--feed-wait`
  • 两个下载大小都是实测而非估猜:OSV 的 npm 导出标为 204 MB 而不是 196,gemnasium 归档标为 52 MB 而不是 80。确认提示会给估算值加上波浪号,以免被误认为服务器报告的大小
  • 看起来像选项的值现在会被拒绝,而不是照单全收——`--out --` 过去会在你的项目里写出一个名为 `--` 的文件并报告成功
  • 当某个版本内容较多时,“新变化”面板不会再溢出到窗口底部之外

公告文档真的送达了——而且计数与你的理解一致

v2.6.0 · 2026年7月29日
  • get_project_advisory 现在返回公告文档本身。此前已连接的客户端只能拿到它的元数据——一个文件名和一个计数——始终拿不到文档,尽管该工具把它描述为一份完整的工作清单
  • 公告导出现在按不同公告各占一条、并合并其来源,而不再按扫描器行各占一条:npm audit 与 OSV 同时报告的同一个漏洞,是一个带上两个公告 ID 的工作项,而不是两条几乎相同的记录。这同样适用于门户的“下载 .md”,计数现在也与仪表盘一致
  • 大到无法放进单个 MCP 响应的项目现在会分页——文档会声明自身不完整,并给出获取其余内容的确切后续调用,而不是被静默截断、让代理把剩下的部分读成“干净”
  • 每个 MCP 工具的每个输入现在都带有描述,新增的 list_mutes 工具会公开 unmute 所需的静音 ID——此前只能通过在同一会话中创建静音才能拿到
  • 修复了严重程度计数的一个缺口:严重程度不属于五个已知取值的发现,会被计入发现总数却不落入任何严重程度分组,于是一个仅有该发现的项目看起来完全干净

通过 MCP 直接获取公告导出

v2.5.0 · 2026年7月28日
  • 已连接的 MCP 客户端可以用新的 get_project_advisory 工具获取项目的完整 Markdown 公告——与门户「下载 .md」按钮生成的文档相同,无需从浏览器复制
  • 被静音的问题不再包含在项目公告导出中,因此代理不会拿到你已经接受风险的工作
  • 注意:公告中包含你的导出提示词,因此 MCP 客户端现在可以读取你在「设置 → 导出」中写下的内容

不再被裁切的弹出层,以及更严格的导出提示词

v2.4.3 · 2026年7月26日
  • 下拉菜单、依赖路径弹出层和公告导出菜单不再被所处的表格或对话框裁切——它们绘制在页面之上,下方空间不足时会向上弹出
  • 默认的公告导出提示词现在要求代理在改动任何文件前先做计划、把可由同一处修复一并解决的发现归为一组,并说明每次版本变更对代码的具体影响;目标是零发现,同时明确排除通往虚假零值的捷径——静音、放宽版本范围或收窄扫描范围——真正无法解决的项则留在带日期的遗留事项表中

分支独立成列

v2.4.2 · 2026年7月25日
  • 扫描项目所用的 git 分支现在在项目列表中独占一列——纯文本、无图标——不再挤在项目名称下方

干净的关闭流程

v2.4.1 · 2026年7月25日
  • 重启容器不会再中断正在写入的扫描,worker 现在会立即启动,而不是先重试约 30 秒
  • 请在 compose 文件中设置 stop_grace_period: 60s(或 --stop-timeout 60)以留出时间——README 和 Docker 文档已补充说明

多语言扫描——Python、Go 和 Rust 加入 npm

v2.4.0 · 2026年7月25日
  • Sentinello 现在除 npm 外还扫描 Python、Go 和 Rust 项目——锁文件完全离线解析,并且每个项目都会报告其扫描覆盖情况(完整、部分或无法审计),让盲区可见而不是悄然存在
  • GitLab 的 gemnasium 数据库加入 npm audit 和 OSV,成为离线公告来源,并通过 CVE/GHSA 别名与其他来源去重;“设置 → 来源”现在是“语言 × 来源”矩阵,可按单元格设置通知范围,并且只要还有一个来源处于启用状态,npm audit 本身也可以关闭
  • 发现结果现在会记录其来源的 git 分支,并显示在项目列表、项目标题栏和每条通知中
  • 项目行自带操作——立即扫描、复制或下载公告、静音或取消静音、编辑标签——因此分诊时不必再逐个进入项目
  • 项目仪表盘从约 3.3 秒降至约 0.03 秒,页面切换现在会显示加载状态,而不是看起来卡住
  • 安全:修复 25 项依赖公告,包括门户图片优化器中真实存在的 libvips CVE,以及影响已发布门户的 9 项 Next.js 公告
  • 默认的公告导出提示词现在涵盖最短发布时长、锁文件校验和过期的 override

更简单的 MCP 设置——无需环境变量

v2.3.0 · 2026年6月9日
  • 现在完全在“设置 → MCP”中配置 MCP:生成令牌即可开启 /api/mcp 端点,清除令牌即可关闭——SENTINELLO_MCP_ENABLED 和 SENTINELLO_MCP_API_TOKEN 环境变量已移除(升级时会一次性导入已有的环境变量令牌)
  • 面向 Claude Code、Codex、Cursor 和 Claude Desktop 的即贴即用连接片段,已预填你的令牌
  • 当通过环境变量设置 SENTINELLO_PORTAL_BASE_URL 时,它会在“设置 → 高级”中以只读方式显示,因为它具有最高优先级并在每次启动时重新应用

更少的误报,以及会自我清理的检测结果

v2.2.0 · 2026年6月9日
  • 恶意软件公告现在会与确切的受影响版本进行比对——曾被入侵的包,其干净或已修复的版本不再被标记
  • 重复的检测结果现在会在下次扫描时自我解决,过期或遗留的条目会自动清除
  • 生产(production)和开发(development)标签现在在所有来源(npm 和 OSV)上以统一的单一方式计算

更简洁的项目页头与一致的筛选器

v2.1.0 · 2026年6月6日
  • 精简的项目页头——在标题旁直接重命名,静音和标签改为图标按钮
  • 在依赖类型筛选器旁新增下拉框,可按来源(npm / OSV)筛选发现
  • 全应用统一一致的下拉框,时区等长列表支持输入即搜索

更清晰的升级指引

v2.0.1 · 2026年6月4日
  • 扩充了 2.0 重大变更的升级步骤
  • README 说明了仅限本地(localhost)的端口绑定

多来源扫描,以及默认安全的加固安装

v2.0.0 · 2026年6月4日
  • 将 OSV 作为可选的第二来源(设置 → 来源,默认关闭),具备恶意软件包检测,并与本地缓存中的公开 OSV 数据库进行比对
  • 检测结果现在可跨来源合并——每个漏洞一行,标记每个来源、提供可用的最佳修复方案以及依赖路径的并集,并配有来源筛选和依赖路径弹出框
  • 安全加固:MCP 端点默认关闭并需要令牌,webhook 投递可防御 SSRF,可选的门户登录入口,容器以非特权用户身份运行
  • “设置”现在是带侧边栏和个人资料页面的顶级板块

MCP 集成与新功能

v1.4.0 · 2026年5月29日
  • 面向 Claude Desktop、Cursor 等客户端的 /api/mcp MCP 服务器
  • 全新的“设置 → MCP”板块,提供服务器 URL 和令牌管理
  • 新功能标记以及发行说明历史

页脚版本显示修复

v1.3.1 · 2026年5月28日
  • 运行中的版本在页脚正确显示

通知改进

v1.3.0 · 2026年5月28日
  • 按环境筛选通知
  • 更简单的通知目标编辑表单
  • 复制现有的通知目标

项目与库页面

v1.2.0 · 2026年5月24日
  • 主页拆分为独立的项目页面和库页面

计划实时重载

v1.1.2 · 2026年5月24日
  • 在门户中保存更改后,worker 会立即重新加载扫描计划

更安全的删除与更清晰的更新横幅

v1.1.0 · 2026年5月23日
  • 删除根目录和通知目标前进行确认
  • 更新提示改为可关闭的顶部横幅
  • 当主机挂载消失时,worker 会清理过期的根目录

扫描器准确性修复

v1.0.1 · 2026年5月23日
  • 丢弃已安装版本实际上不在易受攻击范围内的审计结果
  • 允许删除有发送历史的通知目标

首个开源版本

v1.0.0 · 2026年5月23日
  • Sentinello 的首个公开发布版本

路线图

如今 Sentinello 已跨多种语言盯住你的依赖。这是它的发展方向 —— 以及你可以提出的需求。

更智能的优先级

计划中

按可利用性以及易受攻击的代码是否真正可达对发现进行排序——优先处理重要的问题。

更多集成

计划中

更多通知渠道,以及把 Sentinello 接入你团队已在使用的工具的方式。

静态分析(SAST)

计划中

不仅检测依赖中的已知 CVE,还能发现你自己源码中的风险模式。

密钥与许可证扫描

计划中

在同一个组合、同一个队列中,标记被提交的密钥和许可证问题。

申请集成或数据源

告诉我们你接下来想集成或扫描什么 —— 在 GitHub 上提交 issue,一起塑造路线图。