Auditer la chaîne de dépendances iOS sur un Mac cloud

Auditer la chaîne de dépendances iOS sur un Mac cloud

Un même commit peut réussir hier, puis récupérer aujourd’hui une dépendance transitive différente. Dans les builds exécutés sur un Mac cloud, ce type d’incident est particulièrement difficile à attribuer. En apparence, il peut s’agir d’une simple erreur de compilation, alors que le véritable changement concerne parfois une révision de package, le contenu téléchargé ou l’ajout d’une phase de script. Relancer le build ne suffit pas : chaque exécution doit permettre de répondre à trois questions — qu’est-ce qui a été résolu, le contenu a-t-il changé et ce changement a-t-il été examiné ?

Définir les preuves à conserver

L’audit des dépendances doit au minimum couvrir les dépendances directes et transitives de SwiftPM et CocoaPods. Il est recommandé d’organiser les preuves en trois niveaux :

Niveau Preuves Question traitée
Déclaration Package.swift, Podfile, références du projet Xcode Ce que l’équipe souhaite intégrer
Résolution Package.resolved, Podfile.lock Les versions ou révisions réellement verrouillées
Build Nomenclature, empreintes de fichiers, journaux de build Ce qui a réellement été utilisé pendant cette exécution

Package.resolved et Podfile.lock doivent être versionnés dans le dépôt. Ils ne doivent pas être ignorés dans la CI, et le pipeline ne doit pas poursuivre une publication après les avoir mis à jour automatiquement.

Un fichier de verrouillage ne constitue pas une conclusion de sécurité. Il fournit seulement une entrée stable ; ce sont la revue, les empreintes et l’historique des différences qui justifient l’utilisation de cette entrée dans le build.

Sur les nœuds dédiés de ZoomMini, le répertoire de travail peut être conservé entre les exécutions. L’audit doit néanmoins rester fondé sur les fichiers présents dans le dépôt, et non sur l’état du cache d’une machine particulière comme unique source de preuve.

Utiliser un répertoire de résolution fixe et repartir d’un état propre

Commencez par placer le répertoire de téléchargement de SwiftPM dans l’espace de travail afin d’éviter que différents runners utilisent des caches globaux impossibles à comparer. Pour un projet comprenant un workspace, exécutez :

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

Si le projet ne contient qu’un fichier .xcodeproj, remplacez -workspace par -project. Immédiatement après la résolution, vérifiez que le dépôt n’a pas été modifié :

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

Le build doit être interrompu si cette commande échoue. Les causes courantes sont notamment l’oubli de valider un fichier de verrouillage après la mise à jour d’une dépendance, la réécriture du format par une autre version d’outil ou des contraintes de dépendances autorisant une nouvelle version transitive.

Consigner les limites de la chaîne d’outils

Le dossier d’audit doit également inclure les sorties de xcodebuild -version, swift --version, ruby --version ainsi que les versions des gestionnaires de dépendances. Ces informations ne doivent pas nécessairement figurer dans la nomenclature, mais elles doivent être archivées avec les journaux de build. Sans elles, lorsque le fichier de verrouillage reste identique mais que le comportement de résolution diffère, il devient difficile de déterminer si l’écart provient de la chaîne d’outils ou du code source.

Générer une nomenclature à partir des fichiers de verrouillage

Le script ci-dessous parcourt les fichiers Package.resolved du dépôt. Il prend en charge les structures courantes pins à la racine et object.pins, puis enregistre la version, la branche, la révision et l’empreinte du fichier de verrouillage :

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)

Après son exécution, conservez dependency-manifest.json comme artefact de build. Si l’équipe choisit de le versionner dans le dépôt, il doit être mis à jour dans le même commit que les dépendances. La tâche de publication ne doit jamais l’écraser discrètement.

Transformer la comparaison des différences en barrière de publication

La barrière ne doit pas interdire systématiquement toute modification. Elle doit exiger que chaque changement soit explicable. Voici un ordre d’exécution possible :

  1. Vérifier que les fichiers de déclaration et de verrouillage ont été modifiés ensemble.
  2. Vider le répertoire isolé des dépendances, puis relancer la résolution.
  3. Confirmer que la résolution n’a pas réécrit les fichiers de verrouillage.
  4. Générer une nouvelle nomenclature et la comparer à la référence.
  5. Contrôler la licence, la provenance de maintenance et la capacité à exécuter des scripts de chaque nouvelle dépendance.
  6. Après la compilation et les tests, conserver la nomenclature, les empreintes des fichiers de verrouillage et les versions de la chaîne d’outils.

Des outils de comparaison standard peuvent d’abord servir de barrière automatisée :

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

Lorsqu’une mise à jour est autorisée, ne désactivez pas simplement la barrière. Validez la nouvelle référence dans une branche distincte et demandez aux réviseurs d’examiner en priorité les nouvelles adresses de dépôt, les passages d’une version à une branche, les changements d’empreinte de révision et les dépendances dépourvues de numéro de version.

Examiner particulièrement les phases de script

Les dépendances peuvent exécuter des scripts shell au moyen des phases de build. Lors de l’examen du projet Xcode et des projets de dépendances générés, vérifiez si ces scripts accèdent au réseau, lisent des identifiants situés hors de l’espace de travail, modifient le répertoire des sources ou écrivent des variables d’environnement dans les journaux. Le compte CI ne doit disposer que des autorisations minimales nécessaires pour extraire le code, résoudre les dépendances et exécuter le build.

Gestion des anomalies et contrôles avant livraison

Si l’empreinte de la nomenclature change alors que les fichiers de verrouillage sont restés identiques, interrompez d’abord la publication, puis conservez l’espace de travail, les journaux de résolution et le répertoire de téléchargement. Vérifiez ensuite si des fichiers ont été régénérés, si une dépendance utilise une branche mutable, si le contenu d’un miroir interne a été remplacé ou si la version de l’outil de résolution a changé. Ne commencez pas par vider le cache : vous perdriez les éléments les plus précieux pour l’analyse de l’incident.

Avant chaque livraison, les points suivants peuvent faire l’objet d’un contrôle systématique :

  • Les fichiers de déclaration, les fichiers de verrouillage et la nomenclature appartiennent au même commit.
  • Toutes les dépendances SwiftPM possèdent une version explicite ou une révision immuable.
  • Le résultat de la résolution CocoaPods correspond à Podfile.lock.
  • Les licences et les phases de script des nouvelles dépendances ont été examinées.
  • Une résolution depuis un état propre ne génère aucune différence Git.
  • Les artefacts de build sont associés à l’empreinte de la nomenclature et aux versions de la chaîne d’outils.
  • Aucun jeton ni aucune clé privée sans rapport avec le build n’est exposé aux scripts des dépendances dans la CI.

L’objectif d’un audit de la chaîne de dépendances n’est pas de produire une liste plus longue, mais d’établir une décision reproductible : lorsque les entrées ne changent pas, le résultat peut être vérifié ; lorsqu’elles changent, la publication s’arrête et les réviseurs peuvent identifier précisément où la modification s’est produite.

Questions fréquentes

Un fichier Package.resolved versionné remplace-t-il un inventaire des dépendances ?

Non. Il fixe la résolution SwiftPM, tandis que l’inventaire normalise les identités, versions, révisions et empreintes afin de faciliter la comparaison et l’archivage.

À quel moment exécuter le contrôle des dépendances ?

Exécutez-le avant le build Xcode complet, juste après une résolution propre. Une dérive est ainsi détectée avant l’exécution des phases de script du projet.

Que faire lorsqu’une dépendance transitive change sans demande explicite ?

Suspendez la livraison, identifiez la chaîne parente, examinez la révision et la licence, puis validez le changement dans une branche isolée avec les tests habituels.

ZoomMini Mac dans le cloud

Choisissez un nœud physique dédié pour compiler, tester et expérimenter

Deux configurations M4 sont disponibles sur des nœuds à Singapour, Tokyo, Séoul et Hong Kong. La disponibilité réelle est confirmée par le résultat retourné lors de la commande.

Choisir un modèle et commander