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嵌入, 逆向工具, 配置审计, 静态分析