elastic/supply-chain-monitor
GitHub: elastic/supply-chain-monitor
利用 LLM 自动监控 PyPI 和 npm 顶级包的版本变更,通过 diff 分析检测软件供应链攻击并发出告警。
Stars: 257 | Forks: 25
# 软件供应链监控
[](LICENSE)
自动监控顶级的 **PyPI** 和 **npm** 包以防范供应链攻击。轮询两个注册中心以获取新版本,将每个版本与前一个版本进行 diff 比较,并使用 LLM(通过 [Cursor Agent CLI](https://cursor.com/docs/cli/overview))将 diff 结果分类为 **良性** 或 **恶意**。检测到恶意行为会触发 Slack 警报。
默认情况下会同时监控两个生态系统。使用 `--no-pypi` 或 `--no-npm` 可以禁用其中一个。
## 工作原理
每个生态系统运行各自独立的轮询线程,但共享分析和警报管道。
```
┌─── PyPI ──────────────────────┐ ┌─── npm ───────────────────────┐
│ │ │ │
│ changelog_since_serial() │ │ CouchDB _changes feed │
│ │ │ │ │ │
│ ▼ │ │ ▼ │
│ ┌────────────┐ │ │ ┌────────────┐ │
│ │ All PyPI │─┐ │ │ │ All npm │─┐ │
│ │ events │ │ │ │ │ changes │ │ │
│ └────────────┘ ▼ │ │ └────────────┘ ▼ │
│ hugovk ──► Watchlist │ │ download-counts ─► Watchlist │
│ │ │ │ │ │
│ "new release" events only │ │ new versions since last epoch │
└───────────────┬───────────────┘ └───────────────┬───────────────┘
│ │
▼ ▼
┌───────────────────┐ ┌───────────────────┐
│ Download old + new│ │ Download old + new│
│ (sdist + wheel) │ │ (tarball) │
└───────────────────┘ └───────────────────┘
│ │
└─────────────────┬─────────────────┘
▼
┌───────────────┐
│ Unified diff │
│ report (.md) │
└───────┬───────┘
▼
┌───────────────┐ ◄── LLM analysis
│ Cursor Agent │ (read-only)
│ CLI (ask mode)│
└───────┬───────┘
│
verdict?
│
malicious │
▼
┌───────────────┐
│ Slack alert │
└───────────────┘
```
### 检测目标
LLM 分析会收到提示以查找以下内容:
- 混淆代码(base64、exec、eval、XOR、编码字符串)
- 向意外主机发起的网络调用
- 对启动/持久化位置的文件系统写入
- 进程生成和 shell 命令
- 媒体文件中的隐写术或数据隐藏
- 凭据和 token 窃取
- Typosquatting(仿冒)指标
## 前置条件
- **Python 3.9+** — 使用 `pip install -r requirements.txt` 安装运行时依赖(标准库已包含该工具的大部分功能;`requests` 用于 Slack 上传)
- **Cursor Agent CLI** — 独立的 `agent` 二进制文件,而非 IDE
### 安装 Cursor Agent CLI
**Windows (PowerShell):**
```
irm 'https://cursor.com/install?win32=true' | iex
```
**macOS / Linux:**
```
curl https://cursor.com/install -fsS | bash
```
验证安装:
```
agent --version
```
您必须通过 Cursor 进行身份验证(`agent login` 或设置 `CURSOR_API_KEY`)。
### Slack 配置
将您的 Slack bot token 放在 `etc/slack.json` 中:
```
{
"url": "https://hooks.slack.com/services/...",
"bot_token": "xoxb-...",
"channel": "C01XXXXXXXX"
}
```
该 bot 需要在目标 channel 上具有 `chat:write` 权限。`channel` 字段是发布警报的 Slack channel ID。
## 快速开始
```
# 单次:分析过去约10分钟内的发布
python monitor.py --once
# 持续:监控排名前1000的包(两个 ecosystem),每5分钟轮询一次
python monitor.py --top 1000 --interval 300
# 生产环境:监控排名前15000的包,向 Slack 发送告警
python monitor.py --top 15000 --interval 300 --slack
# 仅 npm,排名前5000
python monitor.py --no-pypi --npm-top 5000
# 仅 PyPI
python monitor.py --no-npm
```
## 文件概述
| 文件 | 用途 |
|------|---------|
| `monitor.py` | **主编排器** — 轮询 PyPI + npm,diff 对比,分析,警报(并行线程) |
| `pypi_monitor.py` | 独立的 PyPI 更新日志轮询器(用于探索) |
| `package_diff.py` | 下载并对任意 PyPI 或 npm 包的两个版本进行 diff 对比 |
| `analyze_diff.py` | 将 diff 发送给 Cursor Agent CLI,解析判定结果 |
| `top_pypi_packages.py` | 按下载量获取并列出前 N 个 PyPI 包 |
| `slack.py` | Slack API 客户端(SendMessage, PostFile) |
| `etc/slack.json` | Slack bot 凭据 |
| `last_serial.yaml` | 持久化的轮询状态(PyPI 序列号 + npm 序列/epoch) |
| `logs/` | 每日日志文件(`monitor_YYYYMMDD.log`) |
## 使用详情
### monitor.py — 主编排器
```
python monitor.py [OPTIONS]
Options:
--top N Number of top packages to watch per ecosystem (default: 15000)
--interval SECS Poll interval in seconds (default: 300)
--once Single pass over recent events, then exit
--slack Enable Slack alerts for malicious findings
--model MODEL Override LLM model (default: composer-2-fast)
--debug Enable DEBUG logging (includes agent raw output)
PyPI options:
--no-pypi Disable PyPI monitoring
--serial N PyPI changelog serial to start from
npm options:
--no-npm Disable npm monitoring
--npm-top N Top N npm packages to watch (default: same as --top)
--npm-seq N npm replication sequence to start from
```
PyPI 和 npm 各自在独立的轮询线程中运行。轮询状态(PyPI 序列号,npm 序列 + epoch)被持久化到 `last_serial.yaml` 中,以便监控器在重启后能从上次中断的位置恢复。
**PyPI 管道:**
1. 从 [hugovk/top-pypi-packages](https://hugovk.github.io/top-pypi-packages/) 数据集加载前 N 个包作为监控列表
2. 连接到 PyPI 的 XML-RPC API 并获取当前序列号
3. 每隔 `--interval` 秒,调用 `changelog_since_serial()` —— 这是一个单次 API 调用,返回上次检查以来的所有事件
4. 筛选出与监控列表匹配的 `"new release"` 事件
5. 对于每个新版本:下载旧版本 + 新版本(当两者都存在时包括 sdist 和 wheel),进行 diff,通过 LLM 分析,如果检测到恶意则在 Slack 发出警报
**npm 管道:**
1. 从 [download-counts](https://www.npmjs.com/package/download-counts) 数据集加载前 N 个包(如果失败则回退到 npm search API)
2. 从 `replicate.npmjs.com` 读取当前的 CouchDB 复制序列
3. 每隔 `--interval` 秒,获取自上次序列以来所有注册中心变更的 `_changes` feed
4. 根据监控列表筛选出发生变更的包,并检查在上次轮询 epoch 之后发布的版本
5. 对于每个新版本:从 npm 注册中心下载新旧 tarball 包,进行 diff,通过 LLM 分析,如果检测到恶意则在 Slack 发出警报
所有输出都会同时记录到控制台和 `logs/monitor_YYYYMMDD.log` 中。
### package_diff.py — 包差异比较器
```
# 比较来自 PyPI 的两个版本
python package_diff.py requests 2.31.0 2.32.0
# 比较来自 npm 的两个版本
python package_diff.py --npm express 4.18.2 4.19.0
# 保存至文件
python package_diff.py telnyx 2.0.0 2.1.0 -o telnyx_diff.md
# 比较本地 archives
python package_diff.py --local old.tar.gz new.tar.gz -n mypackage
```
下载直接通过注册中心 API(PyPI JSON API / npm registry)进行,而不是使用 pip 或 npm。这意味着:
- 下载**不需要依赖 pip/npm**
- **跨平台** — 可以在 Windows 上下载并 diff 仅限 Linux 的包
- PyPI:优先使用 wheel(如果可用则选择纯 Python 包),回退到 sdist
- npm:直接从 registry 下载 tarballs
### analyze_diff.py — LLM 裁决
```
# 分析 diff 文件
python analyze_diff.py telnyx_diff.md
# JSON 输出
python analyze_diff.py telnyx_diff.md --json
# 使用特定的 model
python analyze_diff.py telnyx_diff.md --model claude-4-opus
```
使用 `--trust` 参数以 `--mode ask`(只读)模式运行 Cursor Agent CLI。agent 会读取 diff 文件并返回结构化的判定结果。
退出代码:`0` = 良性,`1` = 恶意,`2` = 未知/错误。
### pypi_monitor.py — 独立轮询器
```
# 查看当前正在发布的内容(过去约10分钟内)
python pypi_monitor.py --once --top 15000
# 持续监控(仅控制台输出,不进行分析)
python pypi_monitor.py --top 1000 --interval 120
```
用于探索 PyPI 发布速度或调试 changelog API,而无需运行完整的分析管道。
### top_pypi_packages.py — 包排名
```
# 打印排名前1000的包
python top_pypi_packages.py
```
```
# 作为 library 使用
from top_pypi_packages import fetch_top_packages
packages = fetch_top_packages(top_n=500)
# [{"project": "boto3", "download_count": 1577565199}, ...]
```
## 数据来源
| 来源 | 内容 | 速率限制 |
|--------|------|-------------|
| [hugovk/top-pypi-packages](https://hugovk.github.io/top-pypi-packages/) | 按 30 天下载量排名的前 15,000 个 PyPI 包(每月 JSON) | 无(静态文件) |
| [PyPI XML-RPC](https://warehouse.pypa.io/api-reference/xml-rpc.html) `changelog_since_serial()` | 实时 PyPI 事件流 | 已弃用但仍可用;每次轮询 1 次调用完全没问题 |
| [PyPI JSON API](https://warehouse.pypa.io/api-reference/json.html) | 包元数据、版本历史、下载 URL | 宽松;使用频率低(每个版本 1 次调用) |
| [download-counts](https://www.npmjs.com/package/download-counts) (nice-registry) | 每个 npm 包的每月下载计数(`counts.json`) | 无(npm tarball) |
| [npm CouchDB replication](https://replicate.npmjs.com) `_changes` feed | 实时 npm 注册中心变更流 | 公开;分页读取 |
| [npm registry API](https://registry.npmjs.org) | 包 packuments、tarball 下载 | 宽松;使用频率低 |
监控器**每个生态系统在每个轮询间隔仅发起 1 次 API 调用**(PyPI changelog / npm `_changes`),外加**每个新版本 2-3 次调用**(版本历史 + 下载)。这是非常轻量级的。
## 警报示例
当监控器检测到恶意版本时,它会将其发布到 Slack:
**PyPI:**
```
🚨 Supply Chain Alert: telnyx 4.87.2
Rank: #5,481 of top PyPI packages
Verdict: MALICIOUS
PyPI: https://pypi.org/project/telnyx/4.87.2/
Analysis summary (truncated):
The changes to src/telnyx/_client.py implement obfuscated
download-decrypt-execute behavior and module-import side effects.
A _d() function decodes base64 strings, a massive _p blob contains
an exfiltration script that downloads a .wav file from
http://83.142.209.203:8080/ringtone.wav and extracts a hidden
payload via steganography...
```
**npm:**
```
🚨 Supply Chain Alert: axios 0.30.4
Rank: #42 of top npm packages
Verdict: MALICIOUS
npm: https://www.npmjs.com/package/axios/v/0.30.4
Analysis summary (truncated):
1. **Non-standard dependency** — The `dependencies` block includes `plain-crypto-js`. Published axios only depends on `follow-redirects`, `form-data`, and `proxy-from-env`. A fourth package whose name looks like a **`crypto-js`–style typosquat** is a classic sign of a tampered or fake package, not a normal axios release.
```
## 局限性
- 每个生态系统线程内的版本是按顺序分析的。在版本发布量较大时,会出现处理积压。
- **必须使用 Cursor Agent CLI** — 分析依赖于有效的 Cursor 订阅以及经过身份验证的 `agent` CLI。
- **沙盒模式**(文件系统隔离)仅在 macOS/Linux 上可用。在 Windows 上,agent 以只读的 `ask` 模式运行,但没有操作系统级别的沙盒保护。
- **监控列表是静态的** — 在启动时从 hugovk (PyPI) 和 download-counts (npm) 数据集加载一次。需要重启才能刷新。
- **npm _changes 间隔保护** — 如果保存的 npm 序列落后于 registry head 超过 10,000 个变更,监控器将重置到 head 以避免漫长的追赶过程。间隔期间的版本将会被遗漏。
## 日志记录
日志会同时写入标准输出和 `logs/monitor_YYYYMMDD.log`。每天会创建一个新文件。两个生态系统都会记录到同一个文件中,npm 的日志行带有 `[npm]` 前缀。示例:
```
2026-03-27 12:01:15 [INFO] Fetching top 15,000 packages from hugovk dataset...
2026-03-27 12:01:16 [INFO] Watchlist loaded: 15,000 packages (dataset updated 2026-03-01 07:34:08)
2026-03-27 12:01:16 [INFO] Fetching top 15,000 npm packages from download-counts dataset...
2026-03-27 12:01:18 [INFO] npm watchlist loaded: 15,000 packages (download-counts 1.0.52)
2026-03-27 12:01:19 [INFO] [pypi] Starting serial: 35,542,068 (from last_serial.yaml) — polling every 300s
2026-03-27 12:01:19 [INFO] [npm] Starting seq: 42,817,503 (from last_serial.yaml) — polling every 300s
2026-03-27 12:06:18 [INFO] [pypi] 2 new watchlist releases detected (serial 35,542,068 -> 35,542,190)
2026-03-27 12:06:18 [INFO] [pypi] Processing fast-array-utils 1.4 (rank #8,231)...
2026-03-27 12:06:18 [INFO] [pypi] Diffing fast-array-utils 1.3 -> 1.4
2026-03-27 12:06:50 [INFO] [pypi] Analyzing diff for fast-array-utils...
2026-03-27 12:07:35 [INFO] [pypi] Verdict for fast-array-utils 1.4: BENIGN
2026-03-27 12:06:20 [INFO] [npm] 1 new watchlist releases detected (seq -> 42,817,612)
2026-03-27 12:06:20 [INFO] [npm] Processing axios 0.30.4 (rank #42)...
2026-03-27 12:06:21 [INFO] [npm] Diffing axios 0.30.3 -> 0.30.4
2026-03-27 12:07:01 [INFO] [npm] Analyzing diff for axios...
2026-03-27 12:07:45 [INFO] [npm] Verdict for axios 0.30.4: MALICIOUS
```
## 贡献、社区与许可
本项目基于 [MIT 许可证](LICENSE) 授权。第三方数据来源和声明已在 [NOTICE.txt](NOTICE.txt) 中总结。
欢迎贡献代码 — 详见 [CONTRIBUTING.md](CONTRIBUTING.md)。本仓库遵循 [Contributor Covenant](CODE_OF_CONDUCT.md)。请通过 [SECURITY.md](SECURITY.md) 报告安全问题,不要使用公开的 issues。
问题与讨论:[Elastic community Slack](https://ela.st/slack)。
标签:CouchDB, DNS 反向解析, GNU通用公共许可证, IP 地址批量处理, LLM, Node.js, npm, PyPI, Python, Slack, Unmanaged PE, 代码差异分析, 包管理器, 开源生态, 无后门, 统一API, 网络信息收集, 网络调试, 自动化, 警报通知, 逆向工具