云端 Mac 的 iOS 依赖供应链审计实战

云端 Mac 的 iOS 依赖供应链审计实战

同一提交在昨天通过、今天却拉到不同的间接依赖,是云端 Mac 构建中很难追责的一类问题。故障表面可能只是编译错误,真正变化的却可能是包修订、下载内容或新增的脚本阶段。处理它不能只靠“重新执行”,而要让每次构建都能回答三个问题:解析了什么、内容是否变化、变化是否经过审核。

先定义需要留下的证据

依赖审计至少覆盖 SwiftPM 与 CocoaPods 的直接、间接依赖。建议把证据分为三层:

层级 证据 解决的问题
声明 Package.swiftPodfile、Xcode 工程引用 团队希望引入什么
解析 Package.resolvedPodfile.lock 实际固定到什么版本或修订
构建 物料清单、文件哈希、构建日志 本次实际使用了什么

Package.resolvedPodfile.lock 必须进入版本库。不要在 CI 中忽略它们,也不要让流水线自动更新后继续发布。

锁文件不是安全结论。它只是稳定输入;审核、哈希与差异记录才说明这份输入为何可以进入构建。

在 ZoomMini 的独享节点上,工作目录可以持续保留,但审计仍应以仓库内文件为准,不能把某台机器缓存中的现状当作唯一证据。

固定解析目录并做干净解析

先把 SwiftPM 下载目录固定到工作区,避免不同 runner 使用无法比较的全局缓存。对包含 workspace 的项目执行:

set -euo pipefail
ROOT="$(pwd)"
SPM_DIR="$ROOT/.audit/spm"
DERIVED_DIR="$ROOT/.audit/DerivedData"

rm -rf "$SPM_DIR" "$DERIVED_DIR"
mkdir -p "$SPM_DIR" "$DERIVED_DIR"

xcodebuild \
  -resolvePackageDependencies \
  -workspace App.xcworkspace \
  -scheme App \
  -clonedSourcePackagesDirPath "$SPM_DIR"

xcodebuild \
  -workspace App.xcworkspace \
  -scheme App \
  -configuration Release \
  -derivedDataPath "$DERIVED_DIR" \
  -clonedSourcePackagesDirPath "$SPM_DIR" \
  build

项目只有 .xcodeproj 时,把 -workspace 换成 -project。解析后立即检查仓库是否被修改:

git diff --exit-code -- '**/Package.resolved' Podfile.lock

这条命令失败时应停止构建。常见原因包括开发者更新依赖后漏交锁文件、不同工具版本重写格式,或依赖约束允许了新的间接版本。

记录工具链边界

审计记录还应包含 xcodebuild -versionswift --versionruby --version 与依赖管理工具版本。它们不一定进入物料清单,但要随构建日志归档。否则锁文件相同而解析行为不同,很难判断差异来自工具链还是源码。

从锁文件生成物料清单

下面的脚本遍历仓库中的 Package.resolved,兼容常见的顶层 pinsobject.pins 结构,并记录版本、分支、修订和锁文件哈希:

import glob
import hashlib
import json
import os

items = []

for path in glob.glob("**/Package.resolved", recursive=True):
    if "/.audit/" in f"/{path}":
        continue
    with open(path, "rb") as handle:
        raw = handle.read()
    data = json.loads(raw)
    pins = data.get("pins") or data.get("object", {}).get("pins", [])
    packages = []
    for pin in pins:
        state = pin.get("state", {})
        packages.append({
            "identity": pin.get("identity") or pin.get("package"),
            "location": pin.get("location") or pin.get("repositoryURL"),
            "version": state.get("version"),
            "branch": state.get("branch"),
            "revision": state.get("revision")
        })
    items.append({
        "file": path,
        "sha256": hashlib.sha256(raw).hexdigest(),
        "packages": sorted(packages, key=lambda item: item["identity"] or "")
    })

for path in glob.glob("**/Podfile.lock", recursive=True):
    with open(path, "rb") as handle:
        raw = handle.read()
    items.append({
        "file": path,
        "sha256": hashlib.sha256(raw).hexdigest()
    })

os.makedirs(".audit", exist_ok=True)
with open(".audit/dependency-manifest.json", "w") as handle:
    json.dump({"artifacts": items}, handle, indent=2, sort_keys=True)

执行后把 dependency-manifest.json 作为构建产物保存。若团队决定将它提交到仓库,就必须在依赖更新提交中同步更新,不能由发布任务悄悄覆盖。

把差异检查变成发布门禁

门禁不应简单禁止所有变化,而应要求变化可解释。一个可执行的顺序是:

  1. 检查声明文件与锁文件是否同时变更。
  2. 清空隔离的依赖目录并重新解析。
  3. 确认解析没有改写锁文件。
  4. 生成新物料清单并与基线比较。
  5. 检查新增依赖的许可证、维护来源和脚本执行能力。
  6. 完成编译与测试后保存清单、锁文件哈希和工具链版本。

可先用标准差异工具形成机器门禁:

python3 tools/dependency_manifest.py
diff -u audit-baseline/dependency-manifest.json .audit/dependency-manifest.json

允许更新时,不要直接关闭门禁。应在独立分支中提交新的基线,并让审核者重点查看新增仓库地址、从版本切换到分支、修订哈希变化,以及没有版本号的依赖。

特别检查脚本阶段

依赖可能通过构建阶段执行 shell 脚本。审核 Xcode 工程与生成的依赖工程时,要确认脚本是否访问网络、读取工作区外的凭据、修改源码目录,或把环境变量写入日志。CI 账户只应获得完成检出、解析和构建所需的最小权限。

异常处理与交付检查项

发现锁文件未变但物料清单哈希变化时,先停止发布,再保留工作区、解析日志与下载目录。随后确认文件是否被重新生成、依赖是否使用可变分支、内部镜像内容是否被替换,以及解析工具版本是否变化。不要先清缓存,否则会丢掉最有价值的现场。

日常交付前可固定检查以下项目:

  • 声明文件、锁文件和物料清单属于同一提交。
  • SwiftPM 依赖均有明确版本或不可变修订。
  • CocoaPods 解析结果与 Podfile.lock 一致。
  • 新增依赖已经过许可证与脚本阶段审核。
  • 干净解析不会产生 Git 差异。
  • 构建产物关联了清单哈希与工具链版本。
  • CI 中未向依赖脚本暴露无关令牌或私钥。

依赖供应链审计的目标不是制造一份更长的清单,而是建立可重复的判断:输入不变时结果可验证,输入变化时发布会停下,并且审核者能准确看到变化发生在哪里。

常见问题

提交 Package.resolved 和 Podfile.lock 后,还需要生成依赖物料清单吗?

需要。锁文件负责固定解析结果,物料清单则把依赖名称、版本、修订、来源字段和文件哈希整理成统一证据,便于审查、归档与跨构建比较。

依赖门禁应该在完整 Xcode 构建前还是构建后执行?

应先执行锁文件完整性与干净解析检查,再进入完整构建。这样可以更早发现未提交的解析变化,并避免在不可信依赖状态上继续执行脚本阶段。

发现间接依赖修订变化时应该如何处理?

暂停发布,确认上游依赖链、变更原因和许可证,再在独立分支中重新解析与测试。审核通过后同时提交锁文件、物料清单和审查记录。

ZoomMini 云端 Mac

为构建、测试与实验选择独享物理节点

两档 M4 配置覆盖新加坡、东京、首尔与香港节点。实际可用状态以下单时返回的结果为准。

选择机型并订购