<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Nisconder</title><link>https://nisconder.pages.dev/</link><description>技术文章、个人项目、阅读与音乐记录。</description><language>zh-CN</language><lastBuildDate>Wed, 26 Aug 2026 15:04:00 GMT</lastBuildDate><atom:link href="https://nisconder.pages.dev/rss.xml" rel="self" type="application/rss+xml" /><item><title>给 npm install 安了一道安检门：npm-safe 的狂飙、错题本与下一站</title><link>https://nisconder.pages.dev/2026/08/26/2026-08-26-blog1/</link><guid isPermaLink="true">https://nisconder-blog.netlify.app/2026/08/26/2026-08-26-blog1/</guid><pubDate>Wed, 26 Aug 2026 15:04:00 GMT</pubDate><description>这是我对 npm-safe 开发过程的一次阶段性复盘。本文对应截至 2026 年 8 月 13 日的 @npm-safe/core v1.0.5</description><content:encoded><![CDATA[<p>前端开发有一种独特的浪漫：为了少写十行代码，我会安装一个包；这个包再带来三十个依赖；三十个依赖背后，可能站着三百位素未谋面的维护者。最后执行 <code>npm install</code> 时，屏幕上一片岁月静好，至于安装脚本在背后忙什么——多少有点像我把家门钥匙交给了刚认识五分钟的网友。</p>
<p>这就是我开发 <code>npm-safe</code> 的起点：在安装真正发生之前，加一道安检门。</p>
<p>我把它设计成一个本地优先的 npm 供应链安全工具：从公共 npm Registry 获取包元数据和 README，用静态规则寻找安装脚本、代码混淆、Base64 Shell、可执行文件直链、仿冒包名、密钥泄露、同形字符和异常 Registry 等风险；再把结果存入本地 SQLite，并通过库 API、命令行、CI、桌面端和 AI agent skill 提供出去。我也接入了可选的 LLM 扫描来补充语义判断，但默认关闭，不影响基础扫描。</p>
<p>一句话概括：我做的不是一套病毒查杀软件，更像是 npm 包登机前的安检员——现在能发现行李里露出来的扳手，却还没有能力把每只袜子都拆开做化验。</p>
<h2 id="两周跑完一个产品周期">两周跑完一个产品周期</h2>
<p>从 2026 年 7 月 29 日到 8 月 13 日，我和贡献者一共向仓库提交了 172 次代码。回头看，这个速度像是把半年路线图压进了两个星期：</p>
<ul>
<li>7 月 29 日，我先搭起核心引擎，完成 Registry、扫描器、SQLite 缓存和调度器等基础层。</li>
<li>7 月 31 日，我补上测试、CLI、中英文输出和代理支持，让它从“一个库”变成“能直接用的工具”。</li>
<li>8 月 1 日，我接入 LLM，又用 Neutralinojs 做出桌面 GUI。安全引擎穿上了 Material You 外套，终于不再只和终端用户谈心。</li>
<li>8 月 2 日，我加入自定义规则插件、规则管理和 AI skill，同时集中修复桌面端注入、误报、TTL 与监控语义等问题。</li>
<li>8 月 3 日到 4 日，我继续补齐 CI 扫描、报告、遥测、共享历史、自动更新、发布流程和安装门禁。</li>
<li>8 月 5 日，我发布了 1.0；随后一路迭代到 1.0.5，重点完善多种 AI agent 的 skill 安装，以及 LLM 输入长度、输出 token 和连接测试配置。</li>
</ul>
<p>从一开始，我就没打算只“做十条正则”。我想让 <code>npm-safe</code> 占住三个入口：开发者主动检查、CI 自动阻断，以及安装动作发生前的强制门禁；桌面端和 AI skill 则负责降低使用门槛。我的判断是，安全工具只有真正出现在工作流里，才不会变成一份躺在抽屉里的消防手册。</p>
<h2 id="我是怎样把它搭起来的">我是怎样把它搭起来的</h2>
<p>我用 <code>pnpm</code> monorepo 组织代码，主体分为 <code>@npm-safe/core</code> 和 Neutralinojs 桌面端。核心引擎的热路径并不复杂：</p>
<ol>
<li>校验包名，从 npm Registry 拉取包信息；请求支持代理、超时、重试和令牌桶限速。</li>
<li>读取最新版本的 <code>package.json</code> 元数据与 README，执行十条内置规则，也可加载本地第三方规则。</li>
<li>从 100 分开始按发现项扣分：Critical 扣 25，High 扣 15，Medium 扣 8，Low 扣 3。</li>
<li>如启用了 LLM，则把静态得分按 60%、LLM 判断按 40% 合成最终分数。</li>
<li>将元数据、报告、监控列表、设置和检查历史写入启用 WAL 的 SQLite。</li>
<li>CLI、桌面 GUI、CI 和安装门禁复用同一个引擎与数据库。</li>
</ol>
<p>我刻意保持各层边界：Registry 客户端不负责缓存，扫描器不负责联网，调度器通过依赖注入组合其他模块，SQLite 也被独立封装。模块各扫门前雪，最后由 <code>NpmSafeEngine</code> 把雪堆成一只完整的雪人。</p>
<h2 id="我的错题本跑得快鞋带就容易松">我的错题本：跑得快，鞋带就容易松</h2>
<h3 id="1-最严重的一课我做的安全工具差点被输入反杀">1. 最严重的一课：我做的安全工具，差点被输入“反杀”</h3>
<p>开发过程中，最让我后怕的一处错误来自桌面端：我曾把 Registry 返回的包名、描述和发现项等内容直接拼进 <code>innerHTML</code>。后来我统一增加了转义，并在提交说明里直白地写下“XSS→RCE”。</p>
<p>这不是普通的页面弹窗恶作剧。我给桌面程序启用了原生 API 和扩展能力，允许列表里还包含 <code>filesystem.*</code>、<code>extensions.*</code>、剪贴板和更新器。一旦不可信的包元数据能注入脚本，攻击面就可能从 DOM 一路滑到本地系统。这个画面很有黑色幽默：我让保安在门口检查别人的行李，却差点忘了关自己的后门。</p>
<p>当前版本已经对渲染字段做了转义，但我不会把“补了一个 <code>escapeHtml()</code>”当作大功告成。下一步我还要减少 <code>innerHTML</code>，尽量使用 <code>textContent</code> 或安全模板，收窄 Neutralino 原生权限，加入 CSP，并把恶意 Registry 响应做成固定安全回归样本。安全边界不能只押宝在一个函数上。</p>
<h3 id="2-我扫描的是简历还不是本人">2. 我扫描的是“简历”，还不是“本人”</h3>
<p>目前，我的静态扫描主要读取 README 与 <code>package.json</code>。它能发现危险安装脚本、可疑描述和仿冒命名，却不会下载并展开 npm tarball，也没有对真实 JavaScript/TypeScript 源码进行 AST、数据流或行为分析。</p>
<p>于是，一个包完全可以把 README 写得像三好学生，把真正的恶意逻辑藏在压缩代码、预构建二进制或深层依赖里。说到底，我现在检查的是嫌疑人的简历，还不是他的行李箱。</p>
<p>所以下一阶段，我会优先校验 tarball 完整性并扫描发布产物，分析生命周期脚本的实际调用链，比较相邻版本的文件、权限和维护者变化，检查锁文件中的间接依赖，再结合来源证明、签名和已知恶意包情报。做到这一步，我才能让 <code>npm-safe</code> 从“元数据风险提示器”真正迈向“供应链扫描器”。</p>
<h3 id="3-分数越低最后却叫未知">3. 分数越低，最后却叫“未知”</h3>
<p>我目前设计的等级映射是：80—100 分为 Safe，50—79 为 Suspicious，20—49 为 Dangerous，0—19 则是 Unknown。现在回头看，这个命名存在明显的认知问题：一个被扣到 10 分的包，不是“我不知道”，而是“我知道的坏事已经有点多了”。</p>
<p><code>Unknown</code> 应表达证据不足，例如 Registry 拉取失败、版本元数据缺失或扫描未完成；它不该成为比分数 20 更糟的风险等级。否则用户可能把“未知”误读成中性状态，严重告警反而失去冲击力。</p>
<p>我准备把“风险”和“置信度”拆成两个维度，并给 Critical 发现设置硬门槛：出现密钥、同形字符或明确恶意行为时，不再让几项低风险指标把平均分调成一杯温吞水。我也需要用真实的恶意包与正常包语料校准评分，公开误报率、漏报率与阈值依据。</p>
<h3 id="4-正则很勤快但也会把路人请进审讯室">4. 正则很勤快，但也会把路人请进审讯室</h3>
<p>我选择用十条内置规则快速覆盖常见模式，但正则与编辑距离天生容易误报。我已经修过“<code>.com</code> 链接被误判为二进制下载”的问题；仿冒包检测也仍依赖一份硬编码的热门包列表和编辑距离，小众生态、作用域包与刻意规避的名称都可能漏网。</p>
<p>接下来我不会只堆规则数量，而会建立规则评测集：让每条规则都有正例、反例、真实 npm 样本和回归案例；在输出中解释“哪段证据触发了什么规则”；再支持项目级基线、忽略理由和到期时间。安全工具如果天天喊狼来了，最后最先被禁用的通常不是狼，而是工具。</p>
<h3 id="5-插件机制很灵活也相当于欢迎自带代码入场">5. 插件机制很灵活，也相当于“欢迎自带代码入场”</h3>
<p>我允许 <code>npm-safe</code> 从 <code>~/.npm-safe/rules/</code> 动态导入 <code>.js</code> 或 <code>.mjs</code> 自定义规则。这对扩展性很好，但我也等于允许插件以当前用户权限执行代码；更糟的是，加载失败时目前还会静默跳过。结果可能是：插件坏了，扫描器没说；插件太“聪明”，系统替它承担后果。</p>
<p>我需要重新明确插件的信任模型，至少增加 manifest、结构校验、来源与哈希记录、错误日志和诊断命令。更进一步，我会考虑隔离进程或受限 DSL，让规则描述“检查什么”，而不是默认获得“想运行什么就运行什么”的能力。</p>
<h3 id="6-llm-是副驾驶暂时不该摸方向盘">6. LLM 是副驾驶，暂时不该摸方向盘</h3>
<p>我为 LLM 扫描接入了 OpenAI、Gemini 和 Anthropic，也允许自定义兼容端点。这扩展了分析能力，同时也给我留下四个必须正视的问题：</p>
<ul>
<li>API Key 会以明文保存在 <code>~/.npm-safe/llm.json</code>，权限设置在 Windows 上只能尽力而为。</li>
<li>README 与包元数据会发送给外部模型，用户需要明确知道数据去了哪里。</li>
<li>输入会被截断，恶意片段如果恰好在预算之外，模型就会非常自信地没看见。</li>
<li>LLM 结果会占最终分数的 40%，但它存在模型漂移、提示词变化和非确定性；数据库缓存键却只区分包、版本和扫描类型，没有记录模型与提示词版本。</li>
</ul>
<p>接下来我会把 LLM 更明确地定位为解释层和补充信号：默认不让它直接降低阻断级别；在报告中记录 provider、model、prompt 版本和输入摘要；让密钥优先进入系统 Keychain、Credential Manager 或环境变量；发送数据前明确展示范围。副驾驶可以提醒我“前面像是有坑”，但刹车踏板仍应该连接到可复现的规则和证据。</p>
<h3 id="7-文档和版本号出现了多人报数">7. 文档和版本号出现了“多人报数”</h3>
<p>我已经把核心包与桌面端发布到 1.0.5，但根目录 <code>package.json</code> 仍停在 0.1.0；架构与 API 文档标题还写着 Phase 1 / v0.1.0；中文 README 的目录示例则标着核心包 v1.0.2。<code>translator/</code> 里也保留着计划到 Phase 5 才实现的骨架，而且没有进入当前门面的主要导出面。</p>
<p>这些问题不影响核心扫描，却说明我的发布收尾还不够严谨，也会让贡献者判断“什么已经交付、什么只是占位”时产生歧义。版本号不统一就像一家人出门旅游，各自说自己住在不同楼层，最后快递员只能在群里喊。</p>
<p>我要建立单一版本来源，在发布流程中自动同步 manifest、桌面配置与文档；把路线图从架构基线文档中拆出来；未实现模块要么进入明确 milestone，要么先移出产品表面。文档也应该尽可能由类型与命令定义生成，减少手工维护造成的漂移。</p>
<h3 id="8-安装门禁很有价值也最容易和操作系统摔跤">8. 安装门禁很有价值，也最容易和操作系统摔跤</h3>
<p>我通过 PATH shim 和 shell profile 包装 <code>npm</code>、<code>pnpm</code>、<code>yarn</code>，让检查发生在真正安装之前。这是我认为产品差异化最强的一步，也是把我拖进操作系统泥潭最深的一步。我已经修过无参数安装、Windows 二进制路径引号、机器级 PATH 优先级和 doctor 判断等问题。</p>
<p>下一步，我会把“显式安全安装命令”和“全局透明拦截”分成清晰模式：前者稳定、可预测，适合 CI 与团队规范；后者方便，但必须保证可逆、可诊断、跨 shell 测试充分，且绝不能误拦截项目脚本。门禁一旦把开发环境堵死，用户通常不会耐心研究安检制度，只会先拆门。</p>
<h2 id="下一站从功能齐全走向判断可信">下一站：从功能齐全走向判断可信</h2>
<p>如果按优先级排路线，接下来我准备这样推进：</p>
<ol>
<li><strong>先加固自己。</strong> 我会收紧桌面端原生权限，补齐 CSP 与恶意输入回归测试，改进密钥保存，并为插件建立明确的信任边界。</li>
<li><strong>再深入包内容。</strong> 我会从 README/元数据扫描扩展到 tarball、源码、生命周期脚本、二进制和版本差异分析，并覆盖间接依赖。</li>
<li><strong>重做风险模型。</strong> 我会分离风险、置信度与扫描完整性，为 Critical 规则提供硬阻断策略，用真实数据集衡量准确率，而不是只看测试数量。</li>
<li><strong>让结果进入团队流程。</strong> 我计划增加 SARIF、CycloneDX/SPDX、策略文件、例外审批和可复现报告，让 GitHub、GitLab 与制品流水线都能消费扫描结果。</li>
<li><strong>治理扩展生态。</strong> 我会为规则和情报源设计签名、版本、兼容性与沙箱机制，逐步形成社区规则库。</li>
<li><strong>最后再扩界面。</strong> 我仍会继续做趋势、版本对比和解释视图，但界面必须服务于证据，而不是把一个未经校准的分数画成更漂亮的仪表盘。</li>
</ol>
<p>这里最重要的取舍，是继续坚持本地优先。我认为这是 <code>npm-safe</code> 区别于大型云安全平台的核心价值之一。未来即使接入社区情报，我也倾向于只下载签名规则与恶意包摘要，把项目依赖和扫描细节留在用户机器上。</p>
<h2 id="结语">结语</h2>
<p>回头看，我已经用 <code>npm-safe</code> 完成了一件不容易的事：没有停在“我有一个安全扫描算法”，而是把它塞进 CLI、CI、桌面端、安装流程和 AI agent 的日常动作里。很多安全项目输在最后一公里，所以我从一开始就在修路。</p>
<p>但我也必须承认：产品面的扩张速度超过了检测深度，安全边界曾被 UI 注入追上，评分语义尚未校准，插件、LLM、门禁和文档里都留着高速开发的接缝。好消息是，我已经把这些错误变成了提交历史里的明确教训；坏消息是，供应链攻击者通常不负责等我写完复盘。</p>
<p>所以，下一阶段我不会只给 <code>npm-safe</code> 再加十个按钮、二十条正则和一个更炫的仪表盘。我要完成的跃迁，是让它从“功能很多的 npm 安检工具”，成长为“判断有证据、结果可复现、自己也经得起攻击的供应链安全基础设施”。</p>
<p>毕竟，我希望做出的保安，不是腰上挂满十种装备，而是真的认得出谁不该进门。</p>
]]></content:encoded><category>技术</category><category>思考</category><category>项目复盘</category></item><item><title>“孤狼强者”：一种无法脱离社会的反社会想象</title><link>https://nisconder.pages.dev/2026/03/21/2026-03-21-blog1/</link><guid isPermaLink="true">https://nisconder-blog.netlify.app/2026/03/21/2026-03-21-blog1/</guid><pubDate>Sat, 21 Mar 2026 11:43:11 GMT</pubDate><description>从“孤狼强者”这一文化意象出发，讨论个体自由、社会承认与权力结构之间无法被简单切断的关系。</description><content:encoded><![CDATA[<p>在很多高武、玄幻乃至现代叙事中，“强者如孤狼”几乎已经成为一种固定意象：他们独来独往、不受约束、凌驾规则之上。乍看之下，这似乎是在强调一种“去社会化”的理想——个体强大到不再需要群体。</p>
<p>但如果稍微深究，就会发现一个颇为有趣的悖论：</p>
<p><strong>“孤狼”这个比喻，本身就是社会性的产物。</strong></p>
<hr>
<h2 id="一孤狼并不来自自然而来自社会">一、“孤狼”并不来自自然，而来自社会</h2>
<p>从生物学角度看，狼并不是典型的独居动物，而是高度群居的代表。正如 查尔斯·达尔文 在进化论视角中强调的那样，物种的生存策略并不只取决于个体强弱，还取决于协作与适应环境的方式。</p>
<p>现代人类学与动物行为学也反复指出：
狼群通过协作狩猎、等级分工来提高整体生存概率。这意味着，“孤狼”并非自然界中的常态，而更像是一个被抽象出来的极端形象。</p>
<p>那么问题来了：既然自然界中并不存在这种理想化的“孤狼”，它从哪里来？</p>
<p>答案是：<strong>它来自人类社会的文化建构。</strong></p>
<p>这种“建构”的思路，可以追溯到 克利福德·格尔茨 所提出的观点——文化本质上是人类赋予世界意义的符号体系。“孤狼”正是这样一个符号：它并不描述现实，而是在表达一种价值取向。</p>
<hr>
<h2 id="二强者孤狼是一种社会叙事">二、“强者=孤狼”是一种社会叙事</h2>
<p>为什么社会会反复生产这种意象？</p>
<p>因为它承载了一种深层张力。正如 托马斯·霍布斯 在《利维坦》中所描述的“自然状态”那样，人类既恐惧无序，又渴望摆脱约束。在他的设想中，如果没有社会契约，人类将陷入“所有人对所有人的战争”。</p>
<p>而“孤狼强者”恰好提供了一种幻想性的解决方案：</p>
<ul>
<li>保留个体的绝对自由</li>
<li>同时避免无序带来的恐惧</li>
</ul>
<p>但这个模型有一个前提：它必须被他人理解和承认。</p>
<p>这里可以引入 埃米尔·涂尔干 的观点——社会事实独立于个体而存在，并对个体产生约束力。“强”这一概念，正是一个典型的社会事实，它依赖评价体系、比较机制与群体共识。</p>
<p>因此：</p>
<blockquote>
<p><strong>“孤狼强者”并不脱离社会，而是建立在社会评价体系之上的一种特殊位置。</strong></p>
</blockquote>
<hr>
<h2 id="三反社会的表达仍然是社会性的">三、反社会的表达，仍然是社会性的</h2>
<p>如果进一步抽象，我们可以借助 卡尔·马克思 的经典论断：</p>
<blockquote>
<p><strong>“人的本质是一切社会关系的总和。”</strong>
——《经济学哲学手稿》</p>
</blockquote>
<p>在这个视角下，“孤狼”并不是一个“脱离关系”的存在，而是一种被关系所定义的“边缘位置”。</p>
<p>马克思的这一思想表明，个体从来不是独立于社会而存在的。在社会中，个人的行为、意志与存在的意义，完全依赖于社会关系的网络。而“孤狼”作为一个符号，依然依赖社会作为对比和评价的基础，甚至其“孤立”地位，也需要社会中的其他个体的认可和理解。</p>
<p>同时，米歇尔·福柯 对权力的分析也提供了重要补充：
权力并不是单向压制，而是弥散在社会网络中的关系结构。所谓“超越规则的强者”，实际上仍然处在权力结构之中，只是占据了不同的位置。</p>
<p>因此我们可以得到一个更清晰的结论：</p>
<blockquote>
<p><strong>“孤狼叙事”并不是反社会，而是社会内部对“超越自身”的一种结构性想象。</strong></p>
</blockquote>
<hr>
<h2 id="四高武世界被放大的社会张力">四、高武世界：被放大的社会张力</h2>
<p>在高武或超能力叙事中，这种结构被进一步放大。</p>
<p>这一点可以用 马克斯·韦伯 关于“合法性支配”的理论来理解：
社会秩序之所以成立，不只是因为暴力，还因为人们对规则的认同。</p>
<p>当个体力量被极度强化时，就会出现一个问题：</p>
<blockquote>
<p><strong>如果个体拥有打破规则的能力，规则还如何维持？</strong></p>
</blockquote>
<p>“孤狼强者”正是对此的叙事回应：</p>
<ul>
<li>他可以突破规则</li>
<li>但规则仍然存在</li>
<li>他成为“例外”，而非“替代”</li>
</ul>
<p>这与 卡尔·施密特 所提出的“例外状态”概念形成某种呼应：
主权者是“决定例外状态的人”，而“孤狼强者”则是被叙事赋予这种权力的个体版本。</p>
<hr>
<h2 id="五个体与社会的永恒张力">五、个体与社会的永恒张力</h2>
<p>如果再往深一层，我们可以借助 弗里德里希·尼采 的思想来理解这种现象。</p>
<p>尼采提出“超人”概念，强调个体对既有价值的超越。但需要注意的是：</p>
<ul>
<li>“超越”必须以既有价值为前提</li>
<li>没有旧价值，就不存在“超越”</li>
</ul>
<p>换句话说：</p>
<blockquote>
<p><strong>所谓“超越社会”，本质上仍然依赖社会作为参照系。</strong></p>
</blockquote>
<p>这也解释了为什么“孤狼强者”总是出现在社会之中，而不是彻底脱离社会的荒野之中——因为一旦脱离参照，它就失去了意义。</p>
<hr>
<h2 id="六总结无法逃离的社会性">六、总结：无法逃离的社会性</h2>
<p>综合以上分析，我们可以将问题压缩为几个层次：</p>
<ol>
<li>生物层面：人类是高度社会性的物种</li>
<li>文化层面：“孤狼”是符号建构</li>
<li>叙事层面：它承载了对自由与秩序的张力</li>
<li>哲学层面：它是一种“以社会为前提的反社会想象”</li>
<li>马克思层面：<strong>个体本质上无法脱离社会关系，它的力量与地位依赖社会结构的认同</strong></li>
</ol>
<p>最终可以归结为一句话：</p>
<blockquote>
<p><strong>“孤狼强者”不是对社会的否定，而是社会对“超越自身”的一种想象。</strong></p>
</blockquote>
<p>而这也反过来说明：</p>
<blockquote>
<p><strong>人无法真正跳出社会，只能在社会结构中不断重新定义自身的位置。</strong></p>
</blockquote>
<hr>
<h2 id="结语">结语</h2>
<p>或许更值得追问的不是“为什么我们崇拜孤狼”，而是：</p>
<blockquote>
<p><strong>我们究竟是在追求脱离社会，还是在追求成为“被社会承认的例外”？</strong></p>
</blockquote>
<p>这两者之间的差别，恰恰构成了现代叙事中最隐秘、也最迷人的张力之一。</p>
<hr>
<h3 id="参考文献">参考文献：</h3>
<ul>
<li>马克思, 卡尔. 《经济学哲学手稿》.</li>
<li>达尔文, 查尔斯. 《物种起源》.</li>
<li>格尔茨, 克利福德. 《文化的解释》.</li>
<li>涂尔干, 埃米尔. 《社会分工论》.</li>
<li>福柯, 米歇尔. 《规训与惩罚》.</li>
<li>韦伯, 马克斯. 《社会学的基本概念》.</li>
<li>施密特, 卡尔. 《政治神学》.</li>
<li>尼采, 弗里德里希. 《查拉图斯特拉如是说》.</li>
</ul>
]]></content:encoded><category>随笔</category><category>思考</category><category>哲学</category><category>社会学</category><category>文化评论</category></item></channel></rss>