Yoyagm/slopguard
GitHub: Yoyagm/slopguard
SlopGuard 是一款零依赖的预安装安全检查工具,可在 pip/npm 安装前检测出 LLM 幻觉包名、拼写抢注包及已知恶意包。
Stars: 0 | Forks: 0
# SlopGuard
[](https://github.com/Yoyagm/slopguard/actions/workflows/slopguard-ci.yml)
[](https://github.com/Yoyagm/slopguard/actions/workflows/codeql.yml)

[](LICENSE)




**状态:** 里程碑 3 已完成 (v0.3.0)。1631 个测试通过,全局覆盖率门槛 ≥90%
且关键包覆盖率 ≥95%。mypy strict,ruff (bandit S),import-linter (7 个契约),
CI 在 GitHub Actions 上运行。
## 它是什么以及检测什么
AI 助手经常建议包含不存在包名的 `pip install` 命令。攻击者会预先注册这些
幻觉产生的名称并植入恶意代码——这种技术被称为 *slopsquatting*——这样开发者在
不经验证的情况下直接复制命令时,就会安装攻击者的包。
SlopGuard 在安装之前检测四类风险,且不执行任何
被分析包的代码:
| 层 | 评估内容 | 产生的信号 |
|---|---|---|
| **第 0 层** — 存在性与年龄 | 查询 PyPI JSON API;验证包是否存在以及存在了多少天 | `NONEXISTENT` (如果返回 404 则直接拦截), `NEW_PACKAGE` (软性) |
| **第 1 层** — *typosquatting* | 针对PyPI top-N 使用 Damerau-Levenshtein + Jaro-Winkler,无网络,确定性 | `TYPOSQUAT` (硬性), `NAME_UNTRUSTED` (名称过长) |
| **第 2 层** — 元数据 | 仅从 PyPI JSON 获取包的质量信号:发布版本、代码库、元数据字段 | `WEAK_METADATA`, `LOW_VERIFIABILITY` (软性,贡献上限为 10 分) |
| **第 3 层** — *threat-intel* | OSV.dev 的 `MAL-*` 安全建议 (已确认恶意) + 可选的 depscope 幻觉观察列表 | `MALICIOUS` (覆盖拦截), `KNOWN_HALLUCINATION` (硬性,权重 85), `THREATINTEL_UNVERIFIABLE` (软性) |
| **第 4 层** — *基于 LLM 的幻觉面* (选择性启用) | 使用 Claude 针对混淆/拼写错误/捏造的分类法,对处于灰区的名称 (年轻/低信号) 进行分类 | `LLM_HALLUCINATION_SURFACE` (独立通道:至多为 `warn`,绝不 `block`), `LLM_UNAVAILABLE` (信息性) |
**零运行时依赖** (仅使用标准库)。PyPI JSON API + OSV.dev (免费,
公开) 以及一个经过 SHA-256 验证的内嵌数据集。**第 4 层 (LLM)** 为 *选择性启用*
(需要 `ANTHROPIC_API_KEY`);默认禁用,其余功能无需 LLM 或付款即可运行。
从 **里程碑 4** 开始,SlopGuard 分析两个功能对等的生态系统:**PyPI** 和
**npm** (`package.json`)。系统会根据清单文件名自动检测生态系统,或者
通过 `--ecosystem {pypi,npm}` 强制指定。分层引擎是**生态系统不可知的**:所有
npm↔PyPI 的差异都存在于适配器中。参见 [生态系统:PyPI 和 npm](#ecosistemas-pypi-y-npm-hito-4)。
## 安装
```
# 在 repository 内(editable 模式,推荐用于开发)
pip install -e .
# 使用开发工具
pip install -e ".[dev]"
```
需要 Python 3.11+。
## CLI 的使用
### 基础扫描
```
slopguard scan requirements.txt
slopguard scan pyproject.toml
pip freeze | slopguard scan - # lee de stdin en formato pip freeze
```
### 人类可读输出示例
```
$ slopguard scan req.txt
SlopGuard 0.1.0 — escaneando req.txt (3 dependencias)
[BLOCK] reqursts
score: 75 | sospechado: requests
Capa 1: El nombre se parece a 'requests' (distancia Damerau-Levenshtein 1).
Accion sugerida: verificar si quiso escribir 'requests'.
[BLOCK] paquete-inexistente-xyz
score: — (override: no existe en PyPI)
Capa 0: El paquete no existe en PyPI (404). Posible alucinacion de LLM.
[ALLOW] boto3
score: 3
Resumen: 1 allow · 0 warn · 2 block · 0 unverifiable
Exit code: 2
```
### JSON 输出示例
```
slopguard scan req.txt --format json
```
```
{
"schema_version": "1.0",
"tool_version": "0.1.0",
"ecosystem": "pypi",
"summary": {
"total": 3,
"allow": 1,
"warn": 0,
"block": 2,
"unverifiable": 0,
"exit_code": 2
},
"error_category": null,
"results": [
{
"name": "paquete-inexistente-xyz",
"version_pin": null,
"status": "ok",
"verdict": "block",
"score": null,
"suspected_target": null,
"error_category": null,
"signals": [
{
"layer": 0,
"code": "nonexistent",
"weight": 0,
"is_soft": false,
"detail": "El paquete no existe en PyPI (404). Posible alucinacion de LLM.",
"suspected_target": null
}
]
},
{
"name": "reqursts",
"version_pin": null,
"status": "ok",
"verdict": "block",
"score": 75,
"suspected_target": "requests",
"error_category": null,
"signals": [
{
"layer": 1,
"code": "typosquat",
"weight": 60,
"is_soft": false,
"detail": "El nombre se parece a 'requests' (distancia 1).",
"suspected_target": "requests"
},
{
"layer": 0,
"code": "new_package",
"weight": 15,
"is_soft": true,
"detail": "Publicado hace 4 dias (umbral 90).",
"suspected_target": null
}
]
},
{
"name": "boto3",
"version_pin": null,
"status": "ok",
"verdict": "allow",
"score": 3,
"suspected_target": null,
"error_category": null,
"signals": []
}
]
}
```
JSON 输出是稳定的、带版本的 (`schema_version`) 且**没有时间戳**
(保证 CI 的确定性)。当判定结果是由于覆盖 (不存在) 或包为 `unverifiable` 时,`score` 字段为 `null`。
## 第 3 层 — Threat-intel (OSV.dev + 可选的 watchlist)
### 通过 OSV.dev 检测恶意包
第 3 层会查询 [OSV.dev](https://osv.dev) —— 一个公开且免费的
*开源安全*建议数据库 —— 以便在安装前检测**已被确认恶意的** Python 包。
只评估在 PyPI 中存在的包 (第 0 层状态为 `FOUND`);不存在的包已经通过覆盖判定为 `block`,不需要进行网络查询。
**拦截标准:**仅带有 `MAL-` 前缀的安全建议 (例如
`MAL-2025-47868`) 会产生 `MALICIOUS` 信号并触发拦截。一般的漏洞
建议 (`GHSA-*`, `CVE-*`, `PYSEC-*`) 在判定时被**忽略** —— SlopGuard 不是
一个 CVE 扫描器,它是*供应链*的守护者。
一个 `MALICIOUS` 信号会强制 `verdict=block` 并通过覆盖使 `score=null`,具有
高于第 0-2 层任何其他判定的**最高优先级**。如果同时存在恶意和拼写错误抢占 (typosquat),这两个信号都会被报告。
### 已知幻觉观察列表 (depscope,可选)
`depscope-hallucinations` 观察列表是一个**选择性启用**的已知幻觉包名
(来自 LLM 的基准测试语料库) 来源,可在
[depscope.dev](https://depscope.dev) 获取。默认情况下它处于未激活状态:
- **当 `enable_watchlist=false` (默认) 时:**不会向 depscope.dev 发起任何查询;
该主机不会被添加到网络的 allowlist 中。
- **当 `enable_watchlist=true` 或使用 `--enable-watchlist` 时:**在运行时获取语料库
(执行 GET 请求,缓存 TTL 为 24 小时)。完全匹配会产生信号 `KNOWN_HALLUCINATION` (硬性信号,
权重 85),从而导致基于分数的 `block`。**包内不会重新分发或内嵌该语料库** (出于对 depscope 的 CC-BY-NC-SA 许可证的尊重)。语料库的归属和许可证信息会显示在输出中。
### 第 3 层的 Flags
```
--no-layer3 Desactiva completamente la Capa 3 (modo solo-deterministas).
Sin red hacia OSV ni depscope. Equivalente al comportamiento
del Hito 1.
--enable-watchlist Activa la consulta opcional a depscope.dev (watchlist de
alucinaciones). Requiere red hacia depscope.dev.
```
这些 flags 也可以通过 `pyproject.toml` 或 `.slopguard.toml` 进行配置:
```
[tool.slopguard]
enable_layer3 = true # default: true
enable_watchlist = false # default: false
```
### 隐私与透明度 (NFR-Priv.3)
SlopGuard 遵循最小化数据暴露原则:
| 发送内容 | 目标主机 | 触发时机 |
|---|---|---|
| 规范化名称 (PEP 503) + 生态系统 (`PyPI`) | `api.osv.dev` | 只要 `enable_layer3=true` 且包存在于 PyPI 中 |
| 仅不带用户参数的 GET 请求 | `depscope.dev` | 仅当 `enable_watchlist=true` 时 |
**绝不发送:**清单文件的内容、本地路径、不必要的固定版本、
用户标识符或任何环境信息。
**无第三方网络模式:**使用 `--no-layer3` (或在配置中设置 `enable_layer3=false`)
以在纯确定性模式下运行。第 0-2 层仅联系 `pypi.org`。
### 第 3 层的默认配置
| 参数 | 默认值 | 描述 |
|---|---|---|
| `enable_layer3` | `true` | 启用/禁用第 3 层 |
| `osv_host` | `api.osv.dev` | OSV API 的主机地址 |
| `osv_ttl_cache_horas` | `6` | 磁盘上 OSV 缓存的 TTL |
| `osv_timeout_total_por_lote_s` | `30` | 每批查询的时间预算 |
| `osv_reintentos` | `2` | 遇到瞬时错误 (5xx/429) 时的重试次数 |
| `osv_batch_max` | `1000` | 每次请求发送给 OSV 的最大包数量 |
| `enable_watchlist` | `false` | 启用 depscope watchlist (选择性开启) |
| `watchlist_host` | `depscope.dev` | watchlist 的主机地址 |
| `watchlist_ttl_cache_horas` | `24` | watchlist 语料库缓存的 TTL |
| `watchlist_timeout_total_s` | `30` | 获取语料库的超时时间 |
| `threatintel_degraded_status` | `unverifiable` | 当 OSV 无响应时的状态 (`unverifiable` 或 `warn`) |
### 安全降级
如果 OSV.dev 或 depscope.dev 没有响应 (超时、5xx、触及速率限制),第 3 层
**绝不会产生虚假的“一切正常”**:它会发出软性信号 `THREATINTEL_UNVERIFIABLE`
并将依赖项的状态降级为 `unverifiable` (exit code 3),同时保留
第 0-2 层确定性判定的结果。由 typosquat 或不存在导致的 `block`
优先于任何 threat-intel 失败。
### 输出示例 — bioql (MAL-2025-47868)
```
$ slopguard scan req.txt
SlopGuard 0.2.0 — escaneando req.txt (2 dependencias)
[BLOCK] bioql
score: — (override: malicia confirmada por OSV)
Capa 3: Reportado como malicioso — MAL-2025-47868
https://osv.dev/vulnerability/MAL-2025-47868
Accion sugerida: no instalar; paquete reportado como malicioso.
[ALLOW] requests
score: 2
Resumen: 1 allow · 0 warn · 1 block · 0 unverifiable
Exit code: 2
```
### JSON 输出示例 (schema_version 1.1)
```
slopguard scan req.txt --format json
```
```
{
"schema_version": "1.1",
"tool_version": "0.2.0",
"ecosystem": "pypi",
"summary": {
"total": 2,
"allow": 1,
"warn": 0,
"block": 1,
"unverifiable": 0,
"exit_code": 2
},
"error_category": null,
"results": [
{
"name": "bioql",
"version_pin": null,
"status": "ok",
"verdict": "block",
"score": null,
"suspected_target": null,
"error_category": null,
"advisories": [
{
"id": "MAL-2025-47868",
"kind": "malicious",
"url": "https://osv.dev/vulnerability/MAL-2025-47868",
"source": "osv"
}
],
"signals": [
{
"layer": 3,
"code": "malicious",
"weight": 0,
"is_soft": false,
"detail": "Reportado como malicioso por OSV (MAL-2025-47868). No instalar.",
"suspected_target": null
}
]
},
{
"name": "requests",
"version_pin": null,
"status": "ok",
"verdict": "allow",
"score": 2,
"suspected_target": null,
"error_category": null,
"advisories": [],
"signals": []
}
]
}
```
`advisories[]` 字段在 schema 1.1 中**始终存在** (如果没有恶意行为则为空)。
此更改是**向后兼容的**:schema 1.0 的消费者会忽略 `advisories` 和
`layer:3` 的信号,不会发生破坏。
## 第 4 层 — 基于 LLM 的幻觉面 (里程碑 3,选择性启用)
第 4 层使用 LLM (Claude `claude-opus-4-8`) 作为针对
*灰区*名称的**可选证实者**:这些包**存在**但**年轻或信号较低**,并且**没有任何
硬性信号** (typosquats、恶意和不存在的包已由第 0–3 层解决)。LLM
根据已计算的**确定性上下文** (年龄、
元数据、软性信号),将名称与 slopsquatting 研究的分类法进行对比分类 ——`legitimo` (合法)、
`conflacion` (混淆)、`typo` (拼写错误)、`fabricacion` (捏造)。绝不发送清单:仅发送 `名称 + 生态系统 + 上下文`。
**第 4 层的信号绝不拦截。**它在一个独立的权重通道中运行,上限为 50 分;在
`SOFT_CAP=25` 和 `umbral_block=80` 的情况下,带有 LLM 信号的依赖项可达到的最大值是
`25 + 50 = 75 < 80`。它最多只会将包提升至 `warn` (建议“在安装前进行验证”),
绝不会提升至 `block` —— 这是由构造保证的 (*gating* 排除了所有硬性信号 ⇒
`max_hard=0`) 并经过属性测试验证。
它是**选择性启用**的:需要 `--enable-layer4` 和环境变量 `ANTHROPIC_API_KEY`。如果没有密钥
或未加上该 flag,SlopGuard 的行为与里程碑 2 完全一致。如果 LLM 没有响应 (超时、
*拒绝*、错误、达到调用上限),该层将**弃权** (`LLM_UNAVAILABLE`):确定性判定
保持不变并报告该警告,而不会假装“一切正常”。
### 第 4 层的 Flags
```
--enable-layer4 Activa la Capa 4 (requiere ANTHROPIC_API_KEY).
--no-layer4 Fuerza la desactivación (default).
--llm-model MODELO Modelo a usar (default: claude-opus-4-8).
```
### 第 4 层的默认配置
| 参数 | 默认值 | 描述 |
|---|---|---|
| `enable_layer4` | `false` | 启用/禁用第 4 层 (选择性启用) |
| `llm_model` | `claude-opus-4-8` | Claude 模型 |
| `gray_edad_max_dias` | `365` | 包被视为“年轻”的最大年龄天数 (*gating* 的分支条件) |
| `llm_conf_min` | `0.5` | 发出风险信号所需的最低置信度 |
| `llm_max_calls_por_corrida` | `50` | 每次运行的网络调用上限 (缓存命中不计入) |
| `llm_ttl_cache_horas` | `168` | LLM 判定缓存的 TTL (标记 `llm-1`) |
## Exit codes (R7)
| 代码 | 含义 | 条件 |
|---|---|---|
| `0` | allow | 所有项结果为 `allow`;无 warn, block 或 unverifiable |
| `1` | warn | 至少有 1 个 `warn`,没有 block 也没有 unverifiable (未使用 `--strict`) |
| `2` | block | 至少有 1 个 `block` (主导信号);或带有 `--strict` 的任何 `warn` |
| `3` | 操作性/unverifiable | 发生全面错误 (清单/配置/数据集) **或** ≥1 个 `unverifiable` 但无 block |
**优先级:**`block (2) > 操作性/unverifiable (3) > warn (1) > allow (0)`。
确认的 `block` 优先于未完成的验证。`unverifiable`
即使 exit code 为 2,也始终会在输出中报告。
## 评分 (ADR-01)
该分数是一个**带有饱和度的累加模型**,它将硬性信号 (基于
名称) 与软性信号 (证实性,有上限) 分开:
```
score = min(100, dura + min(blandas, 25))
```
| 类别 | 信号 / 条件 | 权重 |
|---|---|---|
| 硬性 | TYPOSQUAT — DL = 1 (相差一个字符) | 60 |
| 硬性 | TYPOSQUAT — DL = 2 | 40 |
| 硬性 | TYPOSQUAT — Jaro-Winkler ≥ 0.95 (DL > dl_max) | 30 |
| 硬性 | TYPOSQUAT — 0.92 ≤ JW < 0.95 | 25 |
| 硬性 | NAME_UNTRUSTED — 长度 > `nombre_max_chars` | 30 |
| 软性 | NEW_PACKAGE — 发布少于 `edad_minima_dias` 天 | 15 |
| 软性 | 第 2 层 (WEAK_METADATA + LOW_VERIFIABILITY,硬上限) | ≤ 10 |
**防误报不变性:**软性信号的最大总和为 25,严格
小于 `umbral_warn` (50)。因此,**仅靠软性信号的任何组合**
都无法产生 `warn` 或 `block`。存在且未触发 typosquat 的包
结果始终为 `allow`。
## 配置与默认值 (R8)
SlopGuard 从 `pyproject.toml` 中的 `[tool.slopguard]` 或从
工作目录下的 `.slopguard.toml` 文件加载配置。CLI 的 flags 优先级
高于文件,而文件优先级高于默认值。
**优先级:**CLI flags > 配置文件 > 默认值
### 默认值表
| 参数 | 默认值 | 描述 |
|---|---|---|
| `umbral_block` | `80` | 判定为 `block` 的最低分数 |
| `umbral_warn` | `50` | 判定为 `warn` 的最低分数 |
| `edad_minima_dias` | `90` | 触发 `NEW_PACKAGE` 信号的最小存在天数 |
| `ttl_cache_horas` | `24` | 磁盘缓存的有效期 |
| `concurrencia_max` | `8` | 并行网络 Workers (ThreadPoolExecutor) |
| `connect_timeout_s` | `5` | TCP 连接超时 (秒) |
| `read_timeout_s` | `10` | HTTP 读取超时 (秒) |
| `reintentos_red` | `2` | 遇到瞬时错误时的重试次数 (基础退避 0.5秒) |
| `timeout_total_por_dep_s` | `30` | 每个依赖项的总时间预算 |
| `jw_min` | `0.92` | Jaro-Winkler 相似度的最小阈值 |
| `dl_max` | `2` | 触发信号的 Damerau-Levenshtein 最大距离 |
| `nombre_max_chars` | `100` | 触发 NAME_UNTRUSTED 的最大名称长度 |
| `releases_min` | `1` | 稀少发布版本的阈值 (第 2 层) |
| `metadata_faltantes_min` | `2` | 必须缺失的最小元数据字段数 (第 2 层) |
| `releases_populares` | `10` | 应用第 2 层上限的发布版本阈值 |
| `c2_max_contrib` | `10` | 第 2 层对分数的最大贡献值 |
| `max_manifest_bytes` | `5_000_000` | 清单的最大大小 |
| `max_deps` | `5000` | 每个清单的最大依赖项数量 |
| `max_response_bytes` | `10_000_000` | HTTP 响应限制 (防炸弹) |
| `max_json_depth` | `50` | JSON 的最大深度 (防 JSON 炸弹) |
| `max_include_depth` | `10` | `-r`/`-c` 包含的最大深度 |
### 通过 `pyproject.toml` 配置
```
[tool.slopguard]
umbral_block = 75
umbral_warn = 45
edad_minima_dias = 60
concurrencia_max = 4
```
### 通过 `.slopguard.toml` 配置
```
umbral_block = 75
umbral_warn = 45
```
### 主要 CLI flags
```
--format {human,json} Formato de salida (default: human)
--ecosystem {pypi,npm} Fuerza el ecosistema (default: autodetectado por el manifiesto)
--no-cache Ignora la cache y no escribe en ella
--strict Trata cualquier warn como block (exit 2)
--config Ruta explicita al archivo de configuracion
--manifest-type {requirements,pyproject,freeze} Fuerza el tipo de manifiesto (solo pypi)
--umbral-block N Override del umbral de block
--umbral-warn N Override del umbral de warn
--edad-minima-dias N Override del umbral de edad
--concurrencia N Override del numero de workers
--jw-min F Override del umbral Jaro-Winkler
--dl-max N Override del umbral Damerau-Levenshtein
--no-layer3 Desactiva la Capa 3 (modo solo-deterministas, sin red a OSV)
--enable-watchlist Activa la watchlist de alucinaciones depscope (opt-in)
```
## 生态系统:PyPI 和 npm (里程碑 4)
SlopGuard 分析**PyPI** 和 **npm** 且功能对等 (这四个层的运作方式相同;
仅查询的 registry、typosquatting 数据集和命名规则不同)。
**通过清单自动检测** (R1.2):
| 清单 | 生态系统 |
|---|---|
| `package.json` | npm |
| `requirements*.txt`,任何 `.txt` | pypi |
| `pyproject.toml` | pypi |
**显式覆盖** — `--ecosystem {pypi,npm}` 始终优先于自动检测:
```
slopguard scan package.json # npm (autodetectado)
slopguard scan deps.txt --ecosystem npm # fuerza npm pese al nombre
cat package.json | slopguard scan - --ecosystem npm # stdin EXIGE --ecosystem
```
对于 **stdin** (`-`),没有可用于推断的名称:**必须**提供 `--ecosystem` (R1.5),
如果缺失,则视为可操作的配置错误 (不会假定默认的生态系统)。
npm 的特殊性:
- **Scoped 包** (`@scope/name`):在规范化时会保留 `/` (不进行 PEP 503 式的折叠);
第 1 层仅与**相同 scope** 的候选者进行比较 (防止误报)。
- **非 registry 的 Specifiers** 在扫描时会被**忽略** (不会向 registry 查询):`file:`、
`link:`, `workspace:`, `git://`, `git+…`, `github:`, tarballs `http(s)://` (R2.7)。
- **内嵌的 npm 数据集** (~8000 个名称) 已通过 SHA-256 验证;可以
以可重现的方式重新生成 —— 参见 [npm 数据集操作手册](docs/runbook-dataset-npm.md)。
- 第 0/2 层 `signals[].detail` 的叙述性文本可能会对 npm 依赖显示 "PyPI"
(在 [ADR-0001](docs/adr/0001-texto-ecosistema-en-detail-capas-0-2.md) 中记录的外观性技术债务);
但 JSON 中的结构性字段 `ecosystem` **始终**是正确的。
## 用于 CI 的 JSON 格式 (`schema_version` 1.2)
`schema_version` 字段保证了向前兼容性;更改是
**严格累加的**。演进:`1.0` → `1.1` (增加 `advisories[]` 和信号
`layer:3`) → **`1.2`** (增加根级字段 **`ecosystem`**:`"pypi"` | `"npm"`。
之前的字段不会被修改或删除。
每个结果的稳定字段为:
```
name string nombre normalizado (PEP 503)
version_pin string|null
status "ok" | "unverifiable"
verdict "allow" | "warn" | "block" | null (null si unverifiable)
score integer 0-100 | null (null si unverifiable, inexistente o MALICIOUS)
suspected_target string|null (objetivo del typosquat si aplica)
error_category string|null
advisories[] array de advisories MAL-* (id, kind, url, source); [] si sin malicia [NUEVO 1.1]
signals[] array de seniales con layer/code/weight/is_soft/detail/suspected_target
```
## 在 pre-commit 和 GitHub Actions 中作为 gate 使用
### GitHub Actions
```
- name: SlopGuard — gate de supply chain
run: slopguard scan requirements.txt --strict --format json
# Exit 0: todo ok. Exit 2: block (o warn con --strict). Exit 3: error operacional.
```
```
- name: SlopGuard con reporte JSON
run: |
slopguard scan requirements.txt --format json | tee slopguard-report.json
# El exit code del proceso es el de slopguard
```
### pre-commit
```
# .pre-commit-config.yaml
repos:
- repo: local
hooks:
- id: slopguard
name: SlopGuard — supply chain guard
entry: slopguard scan
args: [requirements.txt, --strict]
language: python
pass_filenames: false
always_run: true
```
## 安全属性
- **零包代码执行:**SlopGuard 仅检查 PyPI 元数据
和 OSV 的安全建议。绝不导入、执行或评估被分析包的
代码。不对外部数据使用 `eval`, `exec`, `pickle` 或 `marshal` (已通过
lint 和 AST 测试验证)。
- **加固的 HTTP:**仅使用 HTTPS 并启用活跃的证书验证 (无法关闭),
扩展到 `{pypi.org, api.osv.dev}` 的主机 *allowlist* (仅在
`enable_watchlist=true` 时包含 `depscope.dev`),无跨方案/跨主机的重定向 — 包括
那些来自 `api.osv.dev` 的重定向 (修复 SSRF:重定向处理器会针对
每个实例的有效集合进行验证),受 `max_response_bytes` 限制的*流式*读取,缓解
解压炸弹和深层 JSON 炸弹 (`max_json_depth`)。
- **Fail-closed (第 3 层):**如果 OSV 或 depscope 没有响应,该依赖项将变为
`unverifiable` (exit 3),绝不会是 `allow`。由确定性分层导致的 block 优先于
任何 threat-intel 失败。
- **带有保护的扩展 Allowlist:**`ALLOWED_HOSTS = {pypi.org}` 依然是
经过静态验证的基础常量;`api.osv.dev` 和 (如果适用) `depscope.dev`
会被以明确且受控的方式按实例添加。静态保护 (AST 测试)
会在 CI 中验证基础集合和有效集合。
- **防止信息源投毒:**OSV 的安全建议 ID 在构造任何
URL 之前会使用正则表达式 `^MAL-[0-9A-Za-z-]+$` 进行验证。名称在
被包含进 POST body 之前会针对字符集进行验证。watchlist 语料库在
接收时和从缓存读取时都会进行验证 (字符集、大小上限、schema)。
- **安全的缓存:**仅使用 JSON (绝不使用 pickle),原子写入 (`os.replace`),
权限 0700/0600,带有命名空间的 SHA-256 密钥 (防 *path traversal*),读取时进行
防御性验证。`UNVERIFIABLE` 状态绝不会持久化存储在缓存中。
- **数据集完整性:**内嵌的 top-N 在加载时通过 SHA-256 进行验证。
- **防止输出注入:**任何名称或外部数据 (包括
OSV/depscope 的 ID 和摘要) 在人类可读输出、日志和 JSON 中
都会经过净化处理 (ANSI CSI/SGR,C0/C1 控制字符,CR/LF)。
- **隐私:**仅向 OSV/depscope 发送规范化名称 + 生态系统。绝不发送
清单、本地路径或用户标识符。可通过 `--no-layer3` 完全禁用。
## 开发
```
python3.11 -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
# Quality gates(在 commit 前必须全部通过)
ruff check .
mypy # strict, 82 archivos
lint-imports # 5 contratos (core/cli, capas/scoring, source, layer3, + hito1)
pytest --cov=slopguard --cov-branch --cov-fail-under=90
```
CI (`.github/workflows/slopguard-ci.yml`) 会在 Python 3.11
和 3.12 上执行相同的 gates,此外还会将 LaTeX 技术文档编译为 PDF。
## 技术文档
完整的技术文档 (分层架构、ADR、数据模型、
时序图、EARS 可追溯性) 位于 `docs/slopguard.tex` 中,并作为 CI 的产物
编译为 PDF。
标签:LNA, Python, 云安全监控, 无后门, 逆向工具, 防护工具, 静态分析