Greenlight架构拆解:Go语言如何实现多扫描器并行运行与统一Finding数据模型
【免费下载链接】greenlightPre-submission compliance scanner for the Apple App Store and Google Play. Scans code, privacy manifests, Android manifests, and IPA/APK/AAB binaries against the review guidelines. Offline, no account.项目地址: https://gitcode.com/gh_mirrors/greenlight2/greenlight
Greenlight 是一个面向 Apple App Store 与 Google Play 的开源上架前合规扫描器:用一条命令离线扫描你的源码、隐私清单、AndroidManifest 和 IPA/APK 产物,输出可追溯到具体审查条款的 Finding(扫描发现)。它的核心架构可以用一句话概括:多个扫描器并行运行 + 一个统一的 Finding 数据模型。下面用 Go 并发和数据结构两个视角拆解这套设计。
一、项目全景:一条命令跑通双商店合规预检
Greenlight 把每个扫描目标封装成独立模块,preflight命令负责把它们编排到一起:
| 扫描器 | 职责 | 源码目录 |
|---|---|---|
| metadata | app.json / Info.plist 元数据检查 | internal/preflight/metadata.go |
| codescan | 24 条规则扫 Swift/ObjC/RN/Expo 源码 | internal/codescan/ |
| privacy | PrivacyInfo.xcprivacy 完整性 | internal/privacy/ |
| playscan | Google Play 政策与截止期限(仅 Android) | internal/playscan/ |
| ipa | IPA 二进制解析 | internal/ipa/ |
结果输出统一交给报告层渲染成终端、JSON 或 SARIF 三种格式,见 internal/report/report.go。
二、并行多扫描器运行:WaitGroup + 互斥锁 + 错误通道
核心编排在 internal/preflight/runner.go 的Run函数里,三个标准库构件撑起整个并发模型:
1.sync.WaitGroup管理生命周期每个扫描器一个 goroutine,启动前wg.Add(1)、结束wg.Done(),主 goroutine 在 runner.go 处wg.Wait()收口。五个扫描器互不阻塞,这也是"中型项目全量预检毫秒级完成"的原因。
2.sync.Mutex保护共享结果所有 goroutine 把发现追加到同一个result.Findings切片,追加前必须mu.Lock()。Go 里切片 append 不是线程安全的,这是多写者汇聚模式的必要防护。
3. 缓冲错误通道收集非致命失败
errs := make(chan error, 8)容量 8 的设计有讲究:错误只会在wg.Wait()之后才被消费,缓冲必须大于最大发送方数量,否则缓冲区写满会直接死锁——源码注释里明确写了这一点(见 runner.go)。
关键设计:失败不丢弃他人结果。某个扫描器崩溃时,它的错误进入errs通道并标记Incomplete = true,但其他扫描器已成功产出的 Finding 照常合并。最后每个失败会被转成一条 WARN 级发现"Scanner did not complete",防止崩溃的扫描器伪装成"无问题"——这对 CI 门禁语义至关重要(见 runner.go)。
平台检测:决定哪些扫描器该运行
并行运行前先回答一个问题:这个项目到底要上哪个商店?DetectPlatforms(internal/preflight/platform.go)通过遍历目录嗅探.xcodeproj、Info.plist、Expo 配置等信号判断 iOS,通过检测 Android 工程判断 Android。判定结果驱动runApple := isIOS || !isAndroid:
- 纯 Android 仓库:跳过所有 Apple 扫描器,避免"缺少 PrivacyInfo.xcprivacy"这类无意义告警;
- 跨平台仓库:两套商店检查在同一次运行中各跑一遍,结果自动合并;
- 无法判定:默认照常运行,宁可多扫不可漏扫。
三、统一 Finding 数据模型:一套结构收编所有扫描器
五个扫描器各有自己的内部结构,例如 codescan 的 Finding、playscan 的 Finding。但对外只暴露一个统一模型,定义在 runner.go:
| 字段 | 含义 | 设计意图 |
|---|---|---|
Source | 来源扫描器(codescan/privacy/ipa/metadata/playscan) | 溯源到具体检查器 |
Severity | CRITICAL / HIGH / WARN / INFO | 四级统一词表,顶层两级触发 CI 失败 |
Guideline | 条款引用 | iOS 用编号("5.1.1"),Android 用政策名("Target API level") |
Title/Detail/Fix | 标题、详情、修复建议 | 每条发现自带可操作出口 |
Doc | 政策页链接 | Play 政策页变更频繁,引用来源比编号更可靠 |
File/Line/Code | 代码定位 | 精确到行,可直接跳转 |
各 goroutine 在自己的锁内把私有结构映射转换成统一Finding再合并(如 playscan 的Policy字段并入Guideline,见 runner.go)。这个"私有结构 → 统一模型"的边界隔离,让下游的汇总统计、去重、JSON/SARIF 序列化只需面对一种类型。
去重:同名发现保留最高严重级别
dedup函数(runner.go)以Title为键折叠跨扫描器的重复发现,并用sevRank映射比较保留 CRITICAL > HIGH > WARN > INFO 中最高的一条,避免同一问题在不同扫描器里各报一次。
codescan 内部还有第二层并行:Scanner.Scan用semaphore 限流(sem := make(chan struct{}, 8))控制文件级并发度,每个文件一个 goroutine 跑全部规则,最后用dedupOnceRules把"项目级事实"类规则(如"全项目无账号删除路径")收敛为单条报告(见 scanner.go)。两级并发 + 两级去重,是这套架构最值得借鉴的细节。
四、结果如何落地:摘要、门禁与 CI 接入
汇总阶段computeSummary按严重级别计数(runner.go),--exit-code在存在 CRITICAL/HIGH 或任一扫描器崩溃(Incomplete)时返回非零——不完整的扫描永远不会被当成通过。
快速上手体验这套架构的效果:
greenlight preflight . # 当前目录全量扫描 greenlight preflight . --format json # 输出 JSON 供 CI 消费 greenlight preflight . --exit-code # 接入 CI 门禁五、总结:三个可复用的架构决策
- 每扫描器一个 goroutine,WaitGroup 收口——简单、可预测,无第三方并发框架;
- 缓冲错误通道容量 > 最大发送方数——避免"Wait 后才消费"场景下的经典死锁;
- 统一 Finding 模型 + 私有结构映射层——下游消费(统计/去重/多格式输出)只依赖一个类型,新增扫描器零成本接入。
这套"并行编排 + 统一数据模型"的组合,正是 Go 写多检查器工具的标准范式,完整源码可从 internal/preflight/runner.go 顺着往下看。
【免费下载链接】greenlightPre-submission compliance scanner for the Apple App Store and Google Play. Scans code, privacy manifests, Android manifests, and IPA/APK/AAB binaries against the review guidelines. Offline, no account.项目地址: https://gitcode.com/gh_mirrors/greenlight2/greenlight
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考