brennhill/sloppy-joe

GitHub: brennhill/sloppy-joe

一款用 Rust 编写的多生态依赖安全扫描工具,在 CI/CD 中拦截 AI 幻觉产生的虚假包和各类抢注攻击。

Stars: 3 | Forks: 0

sloppy-joe

在幻觉拼写错误、抢注和非规范依赖
进入生产环境之前将其拦截。

cargo install sloppy-joe

AI 代码生成器大约有 [20% 的时间](https://arxiv.org/abs/2406.10279) 会产生包名幻觉。攻击者注册这些名称并伺机而动。sloppy-joe 在 CI 中运行 `npm install` 或 `pip install` 之前将其拦截。 ## 如何使用 ``` # 安装(单一静态二进制文件,无运行时依赖) cargo install sloppy-joe # 检查当前项目 — 从 manifest 文件自动检测生态系统 sloppy-joe check # 检查指定目录 sloppy-joe check --dir ./my-project # 仅检查 npm 依赖 sloppy-joe check --type npm # 通过配置强制执行规范规则和组织标准 sloppy-joe check --config /etc/sloppy-joe/config.json # 从 URL 获取配置(在 CI 中有用 — 无需管理机密信息) sloppy-joe check --config https://raw.githubusercontent.com/yourorg/security-configs/main/sloppy-joe.json # 用于 CI 流水线的 JSON 输出 sloppy-joe check --json # 生成初始配置 sloppy-joe init > config.json ``` **退出代码:** `0` = 全部通过,`1` = 发现问题,`2` = 运行时错误。 **支持:** npm, PyPI, Cargo, Go, Ruby, PHP, JVM (Gradle/Maven), .NET — 根据清单文件自动检测。 **配置来源:** 本地文件路径、HTTPS URL 或 `SLOPPY_JOE_CONFIG` 环境变量。配置绝不会从项目目录中读取(原因见 [CONFIG.md](CONFIG.md))。 ## 为什么选择 sloppy-joe? **单一二进制文件。8 个生态系统。16 种攻击类型。生成式检查零误报。AI 智能体无法篡改的配置。** 大多数依赖安全工具只检查一两件事 —— 存在性或编辑距离。sloppy-joe 在单次通过中检查 16 个攻击向量:幻觉包、10 种类型的抢注(同形字、范围抢占、重复字符、分隔符混淆、单词重排、相邻交换、省略字符、混淆形式、大小写变体、版本后缀)、规范强制执行、版本年限门槛、安装脚本放大、依赖爆炸、维护者变更以及通过 OSV.dev 发现的已知漏洞。 它作为单个 Rust 二进制文件运行,没有运行时依赖。它支持所有 8 个主要包生态系统。其配置专为安全而设计:绝不从项目目录读取,可从 URL 加载用于 CI,并在出现问题时提供清晰的错误消息。 | | sloppy-joe | Socket.dev | GuardDog | Phantom Guard | antislopsquat | |---|:---:|:---:|:---:|:---:|:---:| | **存在性检查** | :white_check_mark: | :white_check_mark: | :x: | :white_check_mark: | :white_check_mark: | | **相似度 / 抢注** | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x: | | **同形字检测** | :white_check_mark: | :x: | :x: | :x: | :x: | | **范围抢占** | :white_check_mark: | :x: | :x: | :x: | :x: | | **规范强制执行** | :white_check_mark: | :x: | :x: | :x: | :x: | | **版本年限门槛** | :white_check_mark: | :x: | :x: | :x: | :x: | | **安装脚本放大器** | :white_check_mark: | :white_check_mark: | :x: | :x: | :x: | | **依赖爆炸** | :white_check_mark: | :x: | :x: | :x: | :x: | | **维护者变更** | :white_check_mark: | :white_check_mark: | :x: | :x: | :x: | | **OSV 漏洞检查** | :white_check_mark: | :white_check_mark: | :x: | :x: | :x: | | **配置安全性(库外)** | :white_check_mark: | N/A | :x: | :x: | :x: | | **内部 + 允许列表** | :white_check_mark: | :x: | :x: | :x: | :x: | | **npm** | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x: | | **PyPI** | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | **Cargo** | :white_check_mark: | :white_check_mark: | :x: | :white_check_mark: | :x: | | **Go** | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x: | :x: | | **Ruby** | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x: | :x: | | **PHP** | :white_check_mark: | :large_orange_diamond: | :x: | :x: | :x: | | **JVM (Gradle/Maven)** | :white_check_mark: | :white_check_mark: | :x: | :x: | :x: | | **.NET (NuGet)** | :white_check_mark: | :white_check_mark: | :x: | :x: | :x: | | **单一二进制** | :white_check_mark: | :x: | :x: | :x: | :x: | | **开源** | Apache 2.0 | 商业 | Apache 2.0 | MIT | OSS | | **语言** | Rust | SaaS | Python | Python | Python | :large_orange_diamond: = Beta/实验性 ## 每种攻击的工作原理(以及 sloppy-joe 如何拦截它) ### 1. 幻觉包 **攻击方式:** AI 生成 `import ai_json_helper`。该包不存在。攻击者在 PyPI 上注册 `ai-json-helper` 并植入恶意软件。下次有人运行 `pip install` 时,他们就会下载到恶意包。 **sloppy-joe 如何拦截:** 存在性检查访问 PyPI API 并返回 404。构建被阻止。 ``` ERROR ai-json-helper [existence] Package 'ai-json-helper' does not exist on the pypi registry. It may be hallucinated by an AI code generator. Fix: Remove 'ai-json-helper' from your dependencies. ``` ### 2. 抢注(生成式检查 + 编辑距离回退) **攻击方式:** 攻击者在 npm 上注册 `expresz` —— 与 `express` 仅一个字符之差。AI 生成了它,或者开发人员手误输错了。该包存在,通过存在性检查,并安装恶意软件。 **sloppy-joe 如何拦截:** sloppy-joe 在回退到编辑距离之前运行 10 项生成式检查。每项生成式检查都会生成依赖名称的特定变体(交换字符、折叠重复、去除后缀、重排单词、标准化分隔符、替换同形字、检查范围),并测试是否与已知的热门包完全匹配。这种受 [Rust Foundation 的 Typomania](https://github.com/rustfoundation/typomania) 库启发的方法几乎零误报,因为它仅在变体后产生完全匹配时才会触发。 Levenshtein 编辑距离最后运行,作为针对未被特定检查预见到的新变体的安全网。两者结合,既精确覆盖已知攻击模式,又广泛覆盖未知模式。 ``` ERROR expresz [similarity/edit-distance] 'expresz' is 1 character away from 'express'. This could be a typosquat. Fix: If you meant 'express', fix the name in your manifest. ``` ### 3. 重复字符 **攻击方式:** `expresss`(多了一个 s)或 `reeact`(多了一个 e)。这些是常见的 AI 幻觉模式 —— 模型生成了看起来合理但包含重复字符的名称。 **sloppy-joe 如何拦截:** 重复字符检查每次折叠一个重复字符,并检查结果是否匹配已知包。`expresss` → 移除一个 `s` → `express` → 匹配。 ``` ERROR expresss [similarity/repeated-chars] 'expresss' matches 'express' after removing a repeated character. Fix: Use 'express' — remove the repeated characters. ``` ### 4. 分隔符混淆 **攻击方式:** `python-dateutil` vs `python_dateutil` vs `pythondateutil`。在某些注册表中,这些是不同的包。攻击者注册变体名称。 **sloppy-joe 如何拦截:** 在比较之前标准化所有分隔符(`-`、`_`、`.`)。如果标准化后的形式匹配已知包,则将其标记。 ``` ERROR socket_io [similarity/separator-confusion] 'socket_io' matches 'socket.io' after normalizing separators. Fix: Use the canonical name 'socket.io' with the correct separators. ``` ### 5. 单词重排 **攻击方式:** `parse-json` vs `json-parse`。Levenshtein 距离为 8 —— 对编辑距离检查不可见。但攻击者可以注册重排后的名称。 **sloppy-joe 如何拦截:** 按分隔符分割,生成所有分段排列,并根据语料库检查每一个。`parse-json` → 排列 → `json-parse` → 匹配。 ``` ERROR parse-json [similarity/word-reorder] 'parse-json' is a reordering of 'json-parse'. Fix: Use 'json-parse' — the segments are in the wrong order. ``` ### 6. 相邻字符交换 **攻击方式:** 用 `reqeust` 代替 `request`。两个相邻字符交换位置 —— 这是攻击者武器化的一种常见拼写错误。 **sloppy-joe 如何拦截:** 生成依赖名称的所有相邻交换变体,并根据语料库检查每一个。 ``` ERROR reqeusts [similarity/char-swap] 'reqeusts' matches 'requests' with two adjacent characters swapped. Fix: Use 'requests' — two characters are transposed. ``` ### 7. 省略字符 **攻击方式:** 用 `reqests`(缺少 `u`)代替 `requests`。AI 丢掉了一个字符,结果是一个看起来有效的名称。 **sloppy-joe 如何拦截:** 在名称的每个位置插入每个 a-z 字符,并检查是否有任何结果匹配已知包。`reqests` + 在位置 3 插入 `u` → `requests` → 匹配。 ``` ERROR reqests [similarity/omitted-char] 'reqests' matches 'requests' with one character inserted. Fix: Use 'requests' — a character appears to be missing. ``` ### 8. 同形字(视觉相似字符) **攻击方式:** `rеquests` 使用西里尔字母 `е` (U+0435) 代替拉丁字母 `e` (U+0065)。视觉上完全相同。包名看起来和 `requests` 一模一样,但解析到另一个恶意包。 **sloppy-joe 如何拦截:** 将 17 个已知同形字符(西里尔字母、全角、手写变体)替换为其拉丁等效字符,并检查结果是否匹配已知包。 ``` ERROR rеquests [similarity/homoglyph] 'rеquests' contains characters that look identical to 'requests' but are different Unicode codepoints (homoglyphs). Fix: Replace the lookalike characters with standard ASCII. ``` ### 9. 生态系统混淆形式 **攻击方式:** `py-utils` vs `python-utils`。在 PyPI 上,这些是不同的包。当你想要其中一个时,AI 却生成了另一个。类似地,Go modules 中的 `github.com` vs `gitlab.com`。 **sloppy-joe 如何拦截:** 应用特定于生态系统的替换规则(PyPI 的 py↔python,Go 的 github↔gitlab),并检查是否有任何变体匹配已知包。 ``` ERROR py-flask [similarity/confused-form] 'py-flask' is a confused form of 'flask'. Fix: Use the canonical name 'flask'. ``` ### 10. 大小写变体攻击(区分大小写的注册表) **攻击方式:** 在 Go、Maven 和 Ruby 中,`Rails` 和 `rails` 是不同的包。攻击者注册首字母大写的变体。 **sloppy-joe 如何拦截:** 在区分大小写的注册表上,已知包的任何大小写变体都会被标记为错误。在不区分大小写的注册表(npm, PyPI, Cargo, NuGet, PHP)上,大小写变体是安全的,会被跳过。 ``` ERROR Rails [similarity/case-variant] 'Rails' differs from 'rails' only in letter casing. On case-sensitive registries (ruby) these resolve to different packages. Fix: Use the exact casing 'rails' in your manifest. ``` ### 11. 版本后缀抢注 **攻击方式:** `requests2` 或 `lodash-4`。AI 在包名后追加版本号,而不是正确指定版本。 **sloppy-joe 如何拦截:** 去除尾部的数字和分隔符,并检查基础名称是否匹配已知包。 ``` ERROR requests2 [similarity/version-suffix] 'requests2' looks like 'requests' with a version suffix appended. Fix: Use 'requests' and specify the version in your manifest's version field. ``` ### 12. 范围抢占 **攻击方式:** 攻击者在 npm 上注册 `@typos/lodash` —— 与 `@types/lodash` 仅一个字符之差。或者在 Packagist 上注册 `larvael/framework` —— 与 `laravel/framework` 差两个字符。或者在 Go 上注册 `github.com/gooogle/protobuf` —— 多了一个 `o`。范围乍看之下合法。包能解析。恶意软件被安装。 这种情况罕见但合理 —— 而“罕见但合理”正是 sloppy-joe 存在的意义。2021 年的 `ua-parser-js` 事件就与范围有关。如果它发生在周下载量数百万的包上,它也可能发生在你的包上。 **sloppy-joe 如何拦截:** 从依赖名称中提取范围/命名空间,并使用编辑距离将其与已知良好范围列表进行比较。适用于 npm(`@scope`)、PHP(`vendor/`)、Go(`github.com/org`)和 JVM(`com.group`)。 ``` ERROR @typos/lodash [similarity/scope-squatting] Scope '@typos' is 1 character away from the known scope '@types'. Scope squatting is a known supply chain attack vector. Fix: If you meant '@types/lodash', fix the scope in your manifest. ``` ``` ERROR github.com/gooogle/protobuf [similarity/scope-squatting] Scope 'github.com/gooogle' is 1 character away from 'github.com/google'. Fix: If you meant 'github.com/google/protobuf', fix the org name. ``` ### 13. 非规范包(非攻击 —— 一致性门槛) **攻击方式:** 不是攻击 —— 而是一致性问题。AI 选择了 `moment`,因为它在训练数据中很流行,但你的团队使用 `dayjs`。不同的团队使用不同的包来完成相同的工作,这会造成维护债务和依赖膨胀。 **sloppy-joe 如何拦截:** 你的配置将每个规范包映射到其被拒绝的替代方案。如果依赖项匹配某个替代方案,构建将失败。 ``` ERROR moment [canonical] 'moment' is not the approved package for this purpose. Your team uses 'dayjs'. Fix: Replace 'moment' with 'dayjs' in your manifest file. ``` ### 14. 过新的版本(供应链定时炸弹) **攻击方式:** 攻击者入侵包维护者的账户(或维护者变节)并发布恶意补丁版本。这看起来像正常更新。如果你的 CI 立即安装它,你会在任何人注意到之前受到入侵。 **sloppy-joe 如何拦截:** 版本年限门槛阻止任何发布时间少于 `min_version_age_hours` 小时(默认值:72 小时)的依赖版本。这为社区、Socket.dev 和其他扫描器留出了标记恶意版本的时间。 ### 15. 全新包 **攻击方式:** 昨天创建的包,只有 3 次下载,但名称与流行包相似。极有可能是抢注或未来攻击的占位符。 **sloppy-joe 如何拦截:** 标记任何创建时间少于 30 天的包。 ``` ERROR sketchy-lib [metadata/new-package] 'sketchy-lib' was first published 2 days ago. New packages are higher risk. Fix: Verify 'sketchy-lib' at its registry page and source repository. ``` ### 16. 低下载量包 **攻击方式:** 只有 12 次下载的包,恰好与 `requests` 差一个字符。几乎可以确定是抢注。 **sloppy-joe 如何拦截:** 标记下载量少于 100 的包(针对提供下载数据的注册表 —— 目前包括 npm, crates.io, RubyGems)。 ``` ERROR requsets [metadata/low-downloads] 'requsets' has only 12 downloads. Fix: Verify 'requsets' is the package you intend to use. ``` ## 支持的生态系统 | 生态系统 | 清单文件 | 存在性 | 元数据 | 年限门槛 | |-----------|----------|:---------:|:--------:|:--------:| | npm | package.json | :white_check_mark: | :white_check_mark: | :white_check_mark: | | PyPI | requirements.txt | :white_check_mark: | :white_check_mark: | :white_check_mark: | | Cargo | Cargo.toml | :white_check_mark: | :white_check_mark: | :white_check_mark: | | Go | go.mod | :white_check_mark: | :x: | :x: | | Ruby | Gemfile | :white_check_mark: | :white_check_mark: | :white_check_mark: | | PHP | composer.json | :white_check_mark: | :x: | :x: | | JVM | build.gradle / pom.xml | :white_check_mark: | :white_check_mark: | :white_check_mark: | | .NET | *.csproj | :white_check_mark: | :x: | :x: | 所有生态系统都进行存在性 + 相似度 + 规范性检查。元数据和年限门槛取决于注册表 API 暴露的信息。 ## 快速开始 ``` # 安装 cargo install sloppy-joe # 检查当前项目(自动检测生态系统) sloppy-joe check # 检查并强制执行规范和 age gate sloppy-joe check --config /etc/sloppy-joe/config.json # 为 CI 输出为 JSON sloppy-joe check --json ``` ### 退出代码 | 代码 | 含义 | |------|---------| | `0` | 所有检查通过 | | `1` | 发现问题 | | `2` | 运行时错误 | ## 配置 ``` { "canonical": { "npm": { "lodash": ["underscore", "ramda", "lazy.js"], "dayjs": ["moment", "luxon"], "axios": ["request", "got", "node-fetch", "superagent"] }, "pypi": { "httpx": ["urllib3", "requests"], "ruff": ["flake8", "pylint"] } }, "internal": { "go": ["github.com/yourorg/*"], "npm": ["@yourorg/*"] }, "allowed": { "npm": ["some-vetted-external-pkg"] }, "min_version_age_hours": 72 } ``` **`canonical`** —— 键是已批准的包;值是被拒绝的替代方案。 **`internal`** —— 你组织的包。跳过所有检查。这些包经常变化。 **`allowed`** —— 经过审查的外部包。跳过存在性 + 相似度检查,但仍受版本年限门槛限制。 **`min_version_age_hours`** —— 阻止任何发布时间少于此小时数的版本。默认值:72(3 天)。设置为 0 以禁用。内部包豁免。 ### 配置安全性 配置**绝不从项目目录中读取**。拥有 Shell 访问权限的 AI 智能体可能会重写存储库内的配置,以将其想要的任何内容列入白名单。 配置解析顺序: 1. `--config /path/to/config.json` —— 本地文件(CLI 标志,优先级最高) 2. `--config https://example.com/config.json` —— 从 URL 获取 3. `SLOPPY_JOE_CONFIG=...` —— 环境变量(文件路径或 URL) 4. 无配置 = 仅进行存在性 + 相似度 + 元数据检查 格式错误的配置会**强制失败**并显示可操作的错误消息 —— 损坏的配置绝不会静默回退到无保护状态。 有关完整的格式参考、CI 集成模式和示例,请参阅 [CONFIG.md](CONFIG.md)。 生成模板: ``` sloppy-joe init > /secure/location/config.json ``` ## CI 集成 ### GitHub Actions ``` name: Dependency Guard on: [pull_request] jobs: sloppy-joe: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install sloppy-joe run: cargo install sloppy-joe - name: Check dependencies run: sloppy-joe check --config ${{ secrets.SLOPPY_JOE_CONFIG }} ``` ### GitLab CI ``` dependency-guard: script: - cargo install sloppy-joe - sloppy-joe check --config $SLOPPY_JOE_CONFIG ``` ### Pre-commit Hook ``` #!/bin/sh sloppy-joe check || exit 1 ``` ## 架构 sloppy-joe 采用**生成式优先**的方法进行相似度检测,灵感来自 [Rust Foundation 的 Typomania](https://github.com/rustfoundation/typomania) 库。它不是使用编辑距离将每个依赖项与每个流行包进行比较(这会产生误报),而是生成每个依赖名称的特定变体,并检查是否有任何变体匹配已知包。 ``` For each dependency: 1. Homoglyph normalization → check against corpus 2. Separator normalization → check against corpus 3. Repeated character collapse → check against corpus 4. Version suffix stripping → check against corpus 5. Word reordering → check against corpus 6. Adjacent character swaps → check against corpus 7. Omitted character insertion → check against corpus 8. Ecosystem confused forms → check against corpus 9. Case-variant detection → check against corpus 10. Scope squatting detection → check scope against known-good scopes 11. Levenshtein distance → fallback for novel mutations ``` 生成式检查仅在变体与已知包**完全匹配**时触发 —— 零误报。Levenshtein 最后运行,作为针对无人预见的变体的安全网。 ## 构建基础 - [typomania](https://crates.io/crates/typomania) — Rust Foundation 的抢注检测原语 - [strsim](https://crates.io/crates/strsim) — 字符串相似度度量 ## 测试 167 个测试。涵盖所有相似度检查(包括范围抢占)、元数据信号(安装脚本、依赖爆炸、维护者变更)、OSV 漏洞检查、配置解析、所有 8 个解析器和报告格式。 ``` cargo test ``` ## 许可证 Apache 2.0
标签:AI幻觉检测, Cargo, CI/CD安全, DevSecOps, Golang, Gradle, Llama, Maven, npm, NuGet, OpenVAS, PHP, PyPI, Risk Management, RubyGems, Rust, Slopsquatting, Typosquatting, 上游代理, 云安全监控, 代码安全, 依赖混淆, 包管理器安全, 可视化界面, 安全编程, 左侧安全, 开发者安全, 恶意依赖, 投毒检测, 漏洞枚举, 漏洞测试, 漏洞预防, 漏洞验证, 网络流量审计, 跨平台支持, 软件开发工具包, 通知系统, 静态分析