Aetsu/Nidhogg
GitHub: Aetsu/Nidhogg
一款针对 PyPI 包的静态分析工具,通过提取隐藏的恶意 URL 和校验捆绑的原生二进制文件来检测供应链安全威胁。
Stars: 0 | Forks: 0
# Nidhogg
专注于检测可疑 URL 的 Python 包静态分析器。
它接收已解压的 PyPI 包文件夹,并提取每个候选 URL(字面量、混淆或动态构建的)以及定性检测数据。
## 环境要求
- Python 3.14+
- [uv](https://docs.astral.sh/uv/)
## 安装
```
uv sync
```
## 使用方法
```
# 分析 package
uv run nidhogg.py analyze
# 将 JSON 输出到文件
uv run nidhogg.py analyze --output results.json
# 自定义 benign-domains 列表
uv run nidhogg.py analyze --benign-domains my_domains.txt
# TLS enrichment(需要网络访问)
uv run nidhogg.py analyze --check-ssl
# 扫描捆绑的 native binaries(hash、format、signature)
uv run nidhogg.py analyze --check-binaries
# 批量分析
uv run nidhogg.py analyze --batch --output results.json
# 详细日志
uv run nidhogg.py analyze --verbose
# 从 PyPI 下载并分析单个 package
uv run nidhogg.py fetch requests
# 指定版本,保留下载
uv run nidhogg.py fetch requests --version 2.31.0 --keep-download ./downloads
# 监视 PyPI 的新发布并逐一分析
uv run nidhogg.py monitor --interval 60 --concurrency 8
# 处理最近 10 个新发布的 package 并退出
uv run nidhogg.py monitor --last 10
# 从持久化状态执行单次迭代,然后退出(cron/CI)
uv run nidhogg.py monitor --once --history-dir ./history
# 清理缓存和已保存的文件
uv run nidhogg.py --clean
# 清理缓存 + history 目录
uv run nidhogg.py --clean --history-dir ./history
```
### 可用选项 — `analyze`
| 选项 | 描述 |
|--------|-------------|
| `--json` | 将 JSON 打印到标准输出 |
| `--output PATH` | 将 JSON 写入文件 |
| `--benign-domains PATH` | 自定义安全域名列表 |
| `--check-ssl` | 验证 TLS 证书(需要网络访问) |
| `--check-http` | 探测每个 http/https URL 并记录其状态和页面标题(需要网络访问) |
| `--check-binaries` | 扫描捆绑的原生二进制文件(`.exe`, `.dll`, `.pyd`, `.so`, `.dylib`, `.a`, `.o`):哈希、格式(PE/Mach-O/ELF)以及签名 |
| `--verbose` | 启用调试日志 |
| `--batch` | 将输入视为包含多个包的目录 |
| `--history-dir PATH` | 将每个结果以 JSONL 格式追加到 `/YYYY-MM-DD.jsonl`(默认为项目目录下的 `.cache/nidhogg/history`) |
### `fetch` — 按需单次下载
从 PyPI 下载特定包(`nidhogg/fetching/pypi_fetch.py`),
将其解压到临时目录,并运行相同的分析流水线。
它拥有独立的发现机制,与批处理流程的外部下载器无关。
| 选项 | 描述 |
|--------|-------------|
| `name` | PyPI 包名(位置参数) |
| `--version VERSION` | 指定版本;默认为最新发布版本 |
| `--keep-download [DIR]` | 保留下载/解压的文件,而不是将其删除 |
| `--check-ssl`, `--check-http`, `--check-binaries` | 可选的增强功能(与 `analyze` 相同) |
| `--json`, `--output PATH`, `--history-dir PATH`, `--verbose` | 与 `analyze` 相同 |
### `monitor` — 监视新的 PyPI 发布
循环轮询 PyPI 的 XML-RPC 更新日志(`nidhogg/fetching/changelog.py`),
下载并分析每个新发布的包,并持久化最后处理的序列号
(`nidhogg/fetching/monitor_state.py`),以便它可以在不重新处理
已经看过的发布的情况下恢复运行。如果没有持久化的状态(首次
运行,或在 `--clean` 之后),它会通过回填最近发布的 40 个包来
进行引导,而不是从“现在”开始。
| 选项 | 描述 |
|--------|-------------|
| `--interval SECONDS` | 轮询迭代之间的间隔秒数(默认 300) |
| `--index-file PATH` | 持久化最后处理的序列号的位置(默认为项目目录下的 `.cache/nidhogg/monitor_state.json`) |
| `--concurrency N` | 并行下载/分析的最大包数量(默认 1) |
| `--keep-download DIR` | 将每次下载/解压的内容保留在 DIR 下 |
| `--last N` | 处理最后 N 个新发布的包并退出(无循环) |
| `--once` | 从持久化状态执行单次迭代,然后退出(无循环,无 `time.sleep`)——适用于定时任务(GitHub Actions cron) |
| `--check-ssl`, `--check-http`, `--check-binaries` | 可选的增强功能(与 `analyze` 相同) |
| `--json`, `--history-dir PATH`, `--verbose` | 与 `analyze` 相同 |
### 退出代码
| 代码 | 含义 |
|--------|-------------|
| `0` | 分析完成,没有错误 |
| `2` | 错误(路径无效、读取失败等) |
### `--clean` — 缓存和文件清理
全局标志,用于移除 nidhogg 的持久化数据并立即退出。不需要子命令。
| 清理内容 | 默认路径 |
|------------|------------------|
| 监视器缓存(状态、序列号)+ 默认历史记录 | `.cache/nidhogg/`(包含 `.cache/nidhogg/history/`) |
| 自定义路径的历史记录(如果传递了 `--history-dir`) | 传递给 `--history-dir` 的路径 |
```
# 仅缓存
uv run nidhogg.py --clean
# 缓存 + history
uv run nidhogg.py --clean --history-dir ./history
```
## 网站 (`site/`)
`site/` 是一个静态前端(无需构建步骤),带有日期选择器,可在浏览器中显示结果。Nidhogg 不会直接写入网站要读取的文件:它首先写入 JSONL 历史记录,然后由一个单独的脚本(`scripts/build_site_data.py`)将其转换为 `site/data/*.json`。
```
# 1. 生成 history — 选择一个(或多次运行,它会按天累积)
uv run nidhogg.py monitor --once # real new releases from PyPI
uv run nidhogg.py fetch requests # single package, quick to try out
# 两者都使用默认的 --history-dir: .cache/nidhogg/history
# 2. 重新生成 site/data/index.json + site/data/YYYY-MM-DD.json
uv run python scripts/build_site_data.py .cache/nidhogg/history site/data
# 3. 启动服务并在浏览器中打开
cd site && python3 -m http.server 8000
# 打开 http://localhost:8000
```
重复 1→2 步并刷新浏览器——无需重启服务器。如果在步骤 1 中
使用了自定义的 `--history-dir PATH`,请将相同的 `PATH` 作为
步骤 2 中脚本的第一个参数传递。
在生产环境中,这由
`.github/workflows/monitor.yml`(cron + `workflow_dispatch`)自动运行,它会提交
`site/data/` 并发布到 GitHub Pages。有关数据 schema,请参阅
[`site/README.md`](site/README.md);有关
完整的部署设计,请参阅 [`docs/deployment-github-actions-pages.md`](docs/deployment-github-actions-pages.md)。
## 流水线
```
walker → [layer1_regex + layer2_ast] → aggregator → enrichment(ssl_cert) → output
```
两层分析均并行处理每个 `.py` 文件。结果会
被聚合和丰富,以产生最终的发现。
### 第一层 — 正则表达式
对纯文本进行快速提取:
- 带有 scheme 的 URL(`http`、`https`、`ftp`、`ws`、`wss`)
- 处于网络上下文中的 IPv4(调用 `connect`、`urlopen`、`requests` 等)
- 完整和压缩格式的 IPv6
- 自动过滤私有 IP(RFC 1918 + loopback)
### 第二层 — AST
通过语法树分析静态解析混淆的 URL:
- **常量折叠:** 包含 URL 的字符串字面量
- **二元拼接:** `"http://" + "evil.com"` → 解析为完整的 URL
- **Base64:** `base64.b64decode(Constant)` → 解码并提取
- **F-strings:** 带有可解析部分的 `ast.JoinedStr`
- **作用域跟踪:** 跟踪在使用点之前赋值的变量
### 聚合器
- **URL 清理:** 去除控制字符和非 ASCII 字符,将空格替换为 `%20`
- **URL 验证:** 拒绝具有无效 scheme/netloc 或主机中包含禁止字符(`` {}|\^` ``)的 URL
- **去重:** 为每个唯一的 URL 保留第一次看到的发现
- **标准化:** 域名转为小写,去除片段和末尾斜杠
- **安全域名过滤:** 支持通配符的可配置列表(`pypi.org` 涵盖 `files.pypi.org`)
- **域名分类:** 为每个剩余的 URL 进行威胁分类
### 域名分类
按顺序评估的威胁类别:
| 类别 | 描述 | 示例 |
|-----------|-------------|---------|
| `RAW_IP` | 直接的公共 IP | `185.220.101.x` |
| `SHORTENER` | URL 缩短器 | `bit.ly`, `tinyurl.com`, `t.co` |
| `TUNNELING` | 暴露隧道 | `ngrok.io`, `workers.dev`, `serveo.net` |
| `EXFILTRATION` | 已知的渗透数据接收地 | `discord.com`, `t.me`, `pastebin.com`, `webhook.site` |
| `IP_RECON` | 公共 IP 侦察 | `ipinfo.io`, `ifconfig.me`, `api.ipify.org` |
| `MALWARE_HOSTING` | 匿名文件托管 | `files.catbox.moe`, `gofile.io` |
| `SUSPICIOUS_TLD` | 高风险 TLD | `.tk`, `.ml`, `.zip`, `.xyz`, `.pw` |
### 增强
**SSL/TLS (`--check-ssl`):** 连接到每个 HTTPS 域名的 443 端口并提取证书颁发者。
**HTTP (`--check-http`):** 对找到的每个 http/https URL 执行大小受限的 GET 请求,跟随重定向,并记录最终状态和页面标题。完全没有响应的发现将从结果中剔除。
### 二进制扫描 (`--check-binaries`)
独立于 URL 流水线。遍历包以查找原生
可执行文件/库(`.exe`, `.dll`, `.pyd`, `.so`, `.dylib`, `.a`, `.o`),
使用 SHA-256 对每个文件进行哈希处理,并使用 [LIEF](https://lief.re/) 检测其
真实格式(PE/Mach-O/ELF,依据文件自身的头部,而非其扩展名)
以及它是否带有嵌入式签名——PE 使用 Authenticode,Mach-O 使用代码
签名(ad-hoc 签名会与未签名以及真实签名者明确区分报告);
ELF 没有标准的签名机制,因此始终
报告为未签名。LIEF 无法解析的二进制文件仍会被记录(哈希 +
`unknown` 格式),绝不丢弃。结果会输出到单独的
`/binaries/YYYY-MM-DD.jsonl` 历史记录流中,而非 URL 发现流。
### 历史记录
每个结果都以 JSONL 格式追加到 `/YYYY-MM-DD.jsonl`(`nidhogg/output/history.py`)—— `` 默认为项目目录下的 `.cache/nidhogg/history`;`--history-dir` 会覆盖它。仅执行追加写入;磁盘/权限失败将作为警告记录,绝不中断分析。
## 测试
```
uv run pytest
```
| 文件 | 检查内容 |
|---------|--------------|
| `test_models.py` | Dataclass 实例化和序列化 |
| `test_walker.py` | 包遍历和文件收集 |
| `test_layer1_regex.py` | 通过正则表达式检测 URL 和 IP |
| `test_layer2_ast.py` | 常量折叠、base64、f-strings、作用域跟踪 |
| `test_aggregator.py` | 去重、标准化、域名过滤 |
| `test_domain_classifier.py` | 按域名进行威胁分类 |
| `test_ssl_cert.py` | TLS 证书验证(模拟) |
| `test_http_probe.py` | HTTP 探测、页面标题提取、无响应发现的修剪 |
| `test_file_classifier.py` | 文件角色标记(readme、文档、测试、打包等) |
| `test_output_writer.py` | JSON 序列化和终端输出 |
| `test_renderer.py` | Rich 终端渲染 |
| `test_cli.py` | `analyze`/`fetch`/`monitor` 的连接 |
| `test_integration.py` | 完整的端到端流水线 |
| `test_pypi_fetch.py`, `test_changelog.py`, `test_monitor_state.py` | 发现机制 (`fetching/`) |
| `test_history.py` | 仅追加的 JSONL 历史记录 |
| `test_build_site_data.py` | `history/*.jsonl` → `site/data/*.json` 聚合 |
代码测试夹具以真实的 `.py` 文件形式存在于 `tests/fixtures/` 中,并按场景组织:
- `pkg_basic/` — 字面量 URL、拼接、动态执行
- `pkg_obfuscated/` — base64、f-strings、作用域跟踪
- `pkg_malicioso/` — 真实的 URL 与动态执行混合
### 代码质量
```
uv run ruff check # linting
uv run ruff format # formatting
uv run mypy # strict type checking
```
## 架构
```
nidhogg/
├── core/
│ ├── models.py # Shared dataclasses (PackageAnalysis, UrlFinding, etc.)
│ └── exceptions.py # Project-specific exceptions
├── analysis/
│ ├── walker.py # Main entry point: orchestrates a package's analysis
│ ├── file_classifier.py # File tagging by path/name (readme, docs, test, packaging, ...)
│ ├── layer1_regex.py # Layer 1: regex extraction over plain text
│ ├── layer2_ast.py # Layer 2: constant folding, base64, f-strings, scope tracking
│ ├── aggregator.py # Deduplication, normalization, and domain classification
│ ├── domain_classifier.py # Threat categorization by domain/IP
│ └── binary_scanner.py # Native-binary detection, hashing, and signature check
├── enrichment/
│ ├── ssl_cert.py # TLS certificate verification
│ └── http_probe.py # HTTP probing: response status and page title
├── fetching/
│ ├── pypi_fetch.py # Download + safe extraction of a single PyPI package
│ ├── changelog.py # PyPI XML-RPC changelog client (new releases)
│ └── monitor_state.py # Persists the last serial processed by `monitor`
├── output/
│ ├── writer.py # JSON serialization and terminal output
│ └── history.py # Append-only JSONL history (--history-dir)
├── cli.py # CLI entry point: analyze / fetch / monitor
└── data/
├── suspicious_domains.toml # Threat domains by category
└── benign_domains.txt # ~100 legitimate domains
scripts/
└── build_site_data.py # history/*.jsonl → site/data/*.json + index.json
site/ # Static frontend (see "Website" above)
├── index.html, style.css, app.js
└── data/ # Generated by scripts/build_site_data.py, not by hand
```
标签:DNS 反向解析, Python, 云安全监控, 安全规则引擎, 无后门, 网络信息收集, 自动化payload嵌入, 逆向工具, 配置审计, 静态分析