HexVM 工程實務

在雲端 Mac 建立 iOS 隱私清單稽核閘門

在雲端 Mac 建立 iOS 隱私清單稽核閘門

團隊將 iOS 專案遷移到雲端 Mac 後,建置成功並不代表隱私聲明已完整涵蓋。相依套件升級時可能新增 PrivacyInfo.xcprivacy,資源複製設定也可能導致主專案的清單未被納入最終 App。更穩妥的做法,是將隱私清單視為建置產物的一部分:先檢查原始碼,再檢查產物,最後透過版本化基準阻擋未經審查的變更。

先定義閘門檢查範圍

一套可執行的閘門至少應涵蓋四個層面:清單能否由 plist 解析、頂層欄位型別是否正確、所有相依套件的清單是否都納入檢查範圍,以及最終 App 中是否存在預期的清單。不要只在儲存庫根目錄尋找單一檔案;原始碼相依套件、預編譯框架,以及套件管理器取出的元件,都可能各自附帶聲明。

先在乾淨的工作區中建立清單索引:

find . \
  -path './.git' -prune -o \
  -path './DerivedData' -prune -o \
  -name PrivacyInfo.xcprivacy -print \
  | LC_ALL=C sort > privacy-manifests.current

建議將專案明確維護的路徑寫入 privacy-manifests.baseline。由流水線比較這兩個檔案;如有新增、刪除或路徑變更,應先讓檢查失敗,待程式碼擁有者確認後再更新基準。如此可避免相依套件升級所造成的聲明變更被悄然忽略。

基準並非合規證明。它只能證明目前的變更已經過明確審查,無法取代對程式碼實際行為的判斷。

驗證 plist 與頂層結構

plutil -lint 能找出 XML 或二進位 plist 的損壞問題,但不會代為判斷欄位型別。可以使用 Python 標準函式庫增加一層輕量的結構檢查,無須安裝額外相依套件:

import pathlib
import plistlib
import sys

allowed = {
    "NSPrivacyTracking": bool,
    "NSPrivacyTrackingDomains": list,
    "NSPrivacyCollectedDataTypes": list,
    "NSPrivacyAccessedAPITypes": list,
}

failed = False
files = sorted(pathlib.Path(".").rglob("PrivacyInfo.xcprivacy"))

if not files:
    print("No privacy manifest found")
    sys.exit(1)

for path in files:
    try:
        with path.open("rb") as stream:
            data = plistlib.load(stream)
        if not isinstance(data, dict):
            raise TypeError("Root must be a dictionary")
        for key, value in data.items():
            expected = allowed.get(key)
            if expected is None:
                raise KeyError(f"Unknown top-level key: {key}")
            if not isinstance(value, expected):
                raise TypeError(f"{key} must be {expected.__name__}")
        print(path)
    except Exception as error:
        failed = True
        print(f"{path}: {error}")

sys.exit(1 if failed else 0)

將指令碼儲存為 Scripts/validate_privacy_manifests.py,並在建置前執行。如果專案使用組織內部的擴充欄位,不應直接允許所有未知鍵,而應逐項加入允許清單並記錄其用途。

建立 Required Reason API 複核表

結構正確仍不代表所填理由正確。檔案時間戳記、系統啟動時間、磁碟空間和偏好設定等呼叫,都應納入程式碼複核範圍。自動掃描可以提供線索,但巨集、封裝層與二進位相依套件都可能造成漏報或誤報,因此不要讓單次文字搜尋直接決定理由代碼。

為每項聲明維護一筆可稽核的記錄:

檢查項目 應記錄內容 失敗條件
API 類別 清單中的類別識別碼 類別沒有對應的程式碼路徑
使用位置 模組、檔案與負責人 無法定位呼叫來源
使用目的 提供給使用者的實際功能 理由與行為不一致
相依來源 自有程式碼或具體元件 二進位檔來源不明確
複核觸發條件 程式碼或相依套件版本變更 變更後未重新確認

對於閉源二進位元件,至少應記錄元件版本、清單雜湊值,以及導入該元件的業務功能。若無法解釋聲明的來源,不應靠猜測補齊,而應向相依套件負責人確認。

在最終 App 中再次檢查

原始碼中的檔案可能因 target membership 或資源階段設定錯誤而未被複製。流水線應完成一次未簽署的 Release 建置,再檢查實際產物:

rm -rf .build/privacy

xcodebuild \
  -scheme "$SCHEME" \
  -configuration Release \
  -sdk iphoneos \
  -derivedDataPath .build/privacy \
  CODE_SIGNING_ALLOWED=NO \
  build

APP_PATH="$(find .build/privacy/Build/Products -type d -name '*.app' -print -quit)"
test -n "$APP_PATH"
find "$APP_PATH" -name PrivacyInfo.xcprivacy -print | LC_ALL=C sort

主 App、嵌入式框架與擴充功能應依照專案預期分別出現。不要將 DerivedData 的隨機目錄寫死,也不要只檢查原始碼中的清單數量,因為多個原始碼清單可能在建置過程中被合併、取代或遺漏。

儲存產物清單

將產物中每份清單的相對路徑與 SHA-256 儲存為流水線附件:

find "$APP_PATH" -name PrivacyInfo.xcprivacy -print0 |
  while IFS= read -r -d '' file; do
    relative="${file#"$APP_PATH"/}"
    digest="$(shasum -a 256 "$file" | awk '{print $1}')"
    printf '%s  %s
' "$digest" "$relative"
  done | LC_ALL=C sort > privacy-artifact.sha256

發生審核問題時,這份記錄可以回答「該次建置實際包含哪些內容」,比只查看目前分支更可靠。

將失敗條件納入日常流程

建議將檢查分為快速與完整兩種層級。合併請求先執行清單搜尋、plist 解析、欄位型別與基準差異檢查;主分支再執行 Release 建置及 App 產物複核。快速檢查失敗時,通常可在數十秒內指出問題路徑;完整檢查則負責找出資源階段與嵌入式框架的問題。

上線前保留以下檢查項目:

  • 清單索引與已核准的基準一致。
  • 每個檔案都通過 plutil -lint 與結構檢查指令碼。
  • 新增的 API 類別已關聯至程式碼位置、負責人與實際用途。
  • 相依套件升級後的清單差異已經人工複核。
  • 最終 App、擴充功能與嵌入式框架中的清單符合預期。
  • 產物路徑與雜湊值已隨本次建置封存。

這套閘門的目的並非自動取代團隊作出合規判斷,而是讓遺漏明確轉化為建置失敗,並確保每次聲明變更都能追溯至程式碼、相依套件與審查記錄。

常見問題

專案中已有 PrivacyInfo.xcprivacy 就足夠嗎?

不夠。還要確認檔案可正確解析、欄位型別符合預期,而且確實進入最終 App 或框架建置產物。

自動腳本能判斷每個 Required Reason API 理由是否正確嗎?

不能完全判斷。腳本適合找出缺漏、格式錯誤與未審查變更,理由是否符合實際呼叫情境仍需程式碼負責人複核。

相依套件更新後為什麼要重新產生隱私清單基準?

相依套件可能新增或修改自己的隱私清單。應先審查差異並確認宣告符合實際行為,再更新基準。

獨享 Apple Silicon

為下一次建置選擇獨享雲端 Mac

依工作負載選擇 Mac mini 機型、租期與區域。每筆訂單都對應獨立實體節點,實際可用性以控制台即時回傳結果為準。

選擇雲端 Mac 方案