RPM 包安全特性大规模扫描系统
架构概览
[包列表获取] → [并行下载] → [ELF提取] → [安全扫描] → [结果入库] → [报告生成]
快速开始
1. 安装依赖
# RHEL/Fedora/OpenEuler
sudo dnf install -y parallel checksec sqlite
# 验证
checksec --version
parallel --version
2. 初始化
chmod +x *.sh
./01_init.sh
3. 执行扫描(无锁设计)
# 全量扫描(8 并发)
./02_scan.sh
# 自定义并发数
PARALLEL_JOBS=16 ./02_scan.sh
输出文件:
results/scanned.txt- 已扫描包列表results/success.txt- 扫描成功的包results/failed.txt- 扫描失败的包results/no_binary.txt- 无二进制文件的包results/*.json- checksec 原始结果
4. 导入数据库
python3 04_import_results.py scan_workspace/results/scan_results.db scan_workspace/results
为什么分两步?
- 避免多进程并发写入 SQLite 导致的数据库锁定问题
- 扫描阶段只写文件,完全无锁
- 导入阶段串行处理,安全可靠
5. 生成报告
# 生成统计报告和 CSV
python3 03_report.py scan_workspace/results/scan_results.db --export-csv report.csv
核心设计
1. 预筛选策略
问题: dnf repoquery 默认包含 noarch 包,浪费资源
解决方案:
# 只查询 x86_64 架构(注意格式字符串末尾的 \n)
dnf repoquery --arch x86_64 --qf '%{name}-%{version}-%{release}.%{arch}\n'
# 进一步过滤:检查是否包含目标路径
dnf repoquery --arch x86_64 --list PACKAGE | grep -E '^/(usr/)?(bin|sbin|lib64)'
2. 软链接去重
问题: /bin → /usr/bin 导致重复扫描
解决方案:
# 使用 inode 去重
find . -type f -exec stat -c '%i %n' {} \; | sort -u -k1,1 | cut -d' ' -f2-
3. 并行优化
三级并行:
- 包级: GNU Parallel 并行处理包 (
-j 8) - 下载级: DNF 并行下载配置
- 扫描级: checksec 批量调用
4. 无锁设计(重要)
问题: SQLite 不支持多进程并发写入
解决方案: 扫描与入库分离
# 扫描阶段:多进程并发写文件(无锁)
./02_scan.sh
→ results/scanned.txt
→ results/success.txt
→ results/failed.txt
→ results/*.json
# 导入阶段:单进程串行入库(安全)
python3 04_import_results.py scan_results.db results/
关键改进:
- ✅ 使用
flock文件锁代替数据库查询 - ✅ 状态文件代替数据库状态查询
- ✅ JSON 文件存储中间结果
- ✅ 批量导入代替逐条写入
5. 增量扫描
文件记录扫描状态,跳过已扫描的包:
# 使用 flock 检查是否已扫描
if grep -qx "$pkg_name" "$status_file"; then
echo "跳过已扫描: $pkg_name"
exit 0
fi
6. 错误处理
- 下载超时:
timeout 300s - 解压失败: 记录状态到数据库
- checksec 超时: 单包超时 60s
- 重试机制:
parallel --retries 2
数据库 Schema
packages (id, name, version, status, scan_time)
↓
binaries (id, package_id, file_path, inode)
↓
security_checks (id, binary_id, pie, nx, canary, fortify, relro)
配置文件
编辑 config.sh:
PARALLEL_JOBS=8 # 并发数
REPO_ARCH="x86_64" # 架构
DOWNLOAD_TIMEOUT=300 # 下载超时
CHECKSEC_TIMEOUT=60 # 扫描超时
输出文件
scan_workspace/
├── rpm_cache/ # RPM 缓存
├── extracted/ # 临时解压目录
├── results/
│ ├── scan_results.db # SQLite 数据库
│ ├── *.json # checksec 原始结果
│ └── report.csv # CSV 报告
├── scan.log # 运行日志
├── error.log # 错误日志
└── parallel.log # GNU Parallel 日志
性能估算
- 单包平均耗时: 10-30s (下载 + 解压 + 扫描)
- 8 并发处理 1000 个包: ~30-60 分钟
- 磁盘空间: 每包 10-100MB (临时)
故障排查
1. checksec 找不到
# 从源码安装
git clone https://github.com/slimm609/checksec.sh
sudo cp checksec /usr/local/bin/
2. 并行任务失败
查看日志:
tail -f scan_workspace/error.log
cat scan_workspace/parallel.log | grep -v "^0"
3. 数据库锁定(已解决)
旧版本问题:
Error: in prepare, database is locked (5)
新版本解决方案:
- ✅ 采用无锁设计,扫描阶段不写数据库
- ✅ 使用文件锁
flock代替数据库锁 - ✅ 扫描完成后批量导入
如果仍然遇到数据库锁定:
# 删除锁文件
rm scan_workspace/results/scanned.txt.lock
# 或使用新的工作目录重新扫描
WORK_DIR="./scan_workspace_new" ./02_scan.sh
扩展功能
1. 分布式扫描
# 机器 A: 扫描前 500 个包
head -500 packages.list | parallel -j 8 ./scan_package.sh
# 机器 B: 扫描后 500 个包
tail -500 packages.list | parallel -j 8 ./scan_package.sh
# 合并结果文件(不需要合并数据库)
cat machine_a/results/*.json > all_results.json
cat machine_a/results/success.txt machine_b/results/success.txt > all_success.txt
# 导入到统一数据库
python3 04_import_results.py merged.db results/
优势:
- 每台机器独立工作,无数据库冲突
- 只需合并文本文件和 JSON
- 最后统一导入数据库
2. 定时增量扫描
# crontab
0 2 * * * cd /path/to/scan && ./01_init.sh && ./02_scan.sh && python3 04_import_results.py scan_results.db results/
3. Web Dashboard
使用 Grafana + SQLite 数据源可视化结果
潜在问题与解决方案
| 问题 | 影响 | 解决方案 |
|---|---|---|
| noarch 包误扫描 | 浪费资源 | --arch x86_64 预筛选 |
| 软链接重复 | 结果重复 | inode 去重 |
| 内核模块误报 | PIE 检查不适用 | 排除 *.ko |
| 网络不稳定 | 下载失败 | 超时 + 重试 |
| 磁盘空间不足 | 解压失败 | 及时清理临时文件 |
| checksec 慢 | 扫描耗时长 | 批量调用 + 并行 |
最佳实践
- 首次运行: 先用小样本测试 (
head -10 packages.list) - 并发调优: 根据 CPU 核心数和网络带宽调整
PARALLEL_JOBS - 磁盘管理: 定期清理
rpm_cache/和extracted/ - 日志监控: 实时查看
tail -f scan.log - 增量更新: 定期运行获取新包
参考资料
Description
Languages
Shell
51.4%
Python
48.6%