Mathieu Bernard Réserver 30 min

Due diligence technique : comment je mène les huit jours

On ne voit pas une dette technique dans une démo. On la voit dans l’historique git, les dépendances, la CI et le dernier test de restauration. Voici ma méthode, commande par commande, jusqu’aux clauses du protocole.

Dans une levée, une cession ou un rachat, la due diligence financière et juridique suit une méthode rodée. La partie technique, elle, se résume trop souvent à une démonstration du produit et à une liste de technologies. On ne voit pas une dette technique dans une démo. On la voit dans l’historique git, dans le fichier de verrouillage des dépendances, dans la configuration de la CI et dans la date du dernier test de restauration.

Voici comment je mène concrètement une due diligence technique sur huit jours : ce que je demande, les commandes que je lance, ce qui trompe dans leurs résultats, et comment je transforme les constats en jours de travail puis en clauses. Les sorties montrées sont des exemples génériques, pas celles d’un client.

1. Le cadre : huit jours, en lecture seule

Je demande trois choses avant de commencer, et rien de plus :

  • un accès en lecture aux dépôts (rôle « Reporter » sur GitLab, « Read » sur GitHub), y compris aux dépôts archivés et à la configuration de la CI ;
  • un accès en lecture seule à l’hébergement (par exemple un rôle ReadOnlyAccess sur AWS, ou « Lecteur » sur GCP ou Azure), jamais un accès en écriture, jamais un accès direct à la base de production ;
  • quatre à six entretiens : le dirigeant, le responsable technique, et surtout les développeurs qui écrivent réellement le code.

La lecture seule n’est pas une précaution de forme. Elle protège la cible (je ne peux rien casser), elle me protège (je ne peux pas être tenu pour responsable d’un incident pendant l’audit), et elle force l’équipe à me montrer elle-même ce que je ne peux pas lancer, comme une restauration de sauvegarde. Ce qu’elle refuse de montrer est déjà un constat.

Je travaille sur un clone miroir, pour avoir toutes les branches et tous les tags, pas seulement la branche principale :

git clone --mirror git@gitlab.example.com:cible/plateforme.git
git clone plateforme.git plateforme && cd plateforme
git branch -r | wc -l          # nombre de branches distantes
git tag --sort=-creatordate | head -5   # dernières versions publiées

Le déroulé type sur huit jours :

JourCe que je fais
1Entretien de cadrage, ouverture des accès, inventaire des dépôts, des environnements et des comptes
2Historique git : qui écrit le code, à quel rythme, où se concentrent les changements
3Volume et structure du code, versions des langages et des frameworks
4Dépendances : vulnérabilités, retard de versions, licences, secrets dans l’historique
5Analyse statique, tests, couverture, lecture de la CI et du déploiement
6Infrastructure, sauvegardes et test de restauration, supervision
7Entretiens avec les développeurs, vérification des constats, chiffrage en jours
8Rédaction du rapport et restitution

Le périmètre peut descendre à cinq jours pour un seul dépôt, ou monter à dix pour plusieurs produits. Mais l’ordre ne change pas : les mesures d’abord, les entretiens ensuite. Si je fais les entretiens en premier, j’audite le discours de l’équipe au lieu d’auditer le code.

2. L’historique git : qui sait, et à quel rythme

La première question d’une due diligence n’est pas « le code est‑il bon ? » mais « qui le comprend ? ». L’historique git y répond mieux que n’importe quel organigramme.

git shortlog -sn --no-merges --since="12 months ago"

Une sortie type :

  1412  Développeur A
   187  Développeur B
    64  Prestataire C
    12  dependabot[bot]

Ici, une personne porte plus de 80 % des commits de l’année. C’est le bus factor : le nombre de personnes qu’il faut perdre pour que plus personne ne sache faire évoluer le produit. À un, le risque est critique, quelle que soit la qualité du code.

Trois pièges avant de conclure :

  • Les identités multiples. Le même développeur commite avec son adresse personnelle et son adresse professionnelle. Je vérifie avec git shortlog -sne et je regroupe les alias dans un fichier .mailmap local avant de compter.
  • Les squash merges. Si l’équipe fusionne ses branches en un seul commit, l’auteur affiché est parfois celui qui a cliqué sur « Merge », pas celui qui a écrit le code. Je recoupe avec les demandes de fusion dans GitLab ou GitHub.
  • Les commits ne sont pas du savoir. Quelqu’un qui fait beaucoup de petits commits n’est pas forcément celui qui connaît la facturation. Je regarde donc qui touche les fichiers critiques :
git log --since="24 months ago" --format='%an' -- src/Billing/ | sort | uniq -c | sort -rn

Ensuite, le rythme. Le nombre de commits par mois sur deux ans montre si le produit vit, ralentit ou a été abandonné puis relancé :

git log --since="24 months ago" --no-merges --format='%ad' --date=format:'%Y-%m' \
  | sort | uniq -c

Une courbe qui s’effondre six mois avant la cession, c’est souvent un développeur parti, ou une équipe occupée à préparer la vente plutôt qu’à faire évoluer le produit. Une courbe régulière avec des pics avant chaque version est plutôt bon signe.

Enfin, les fichiers qui changent le plus :

git log --since="12 months ago" --no-merges --name-only --format='' \
  | grep -v '^$' | sort | uniq -c | sort -rn | head -15

Un même contrôleur de 3 000 lignes modifié deux cents fois dans l’année, c’est le point chaud : là où chaque évolution coûte le plus cher, et là où les régressions naissent. C’est la meilleure mesure que je connaisse de la question « combien coûte une évolution aujourd’hui ? ».

3. Le volume et la forme du code

Je mesure le volume avec cloc ou tokei, en excluant le code des dépendances :

cloc . --exclude-dir=vendor,node_modules,var,public/build
# ou
tokei . --exclude vendor --exclude node_modules
Language        files     blank   comment      code
PHP              1840     31200     18400    142000
JavaScript        310      4100      1900     28500
Twig              420      2100       120     14800
YAML               95       300        80      4200

Le chiffre seul dit peu. Ce qui compte, c’est ce qu’on trouve en le regardant de près. Un dossier lib/ qui contient une bibliothèque copiée à la main au lieu d’être installée par Composer : elle ne sera jamais mise à jour, et composer audit ne la verra pas. Du JavaScript minifié commité dans le dépôt : cloc le compte comme du code écrit par l’équipe. Je vérifie toujours les plus gros fichiers :

find src -name '*.php' -exec wc -l {} + | sort -rn | head -10

Puis les versions. Je relève la version du langage et du framework dans composer.json, package.json, les Dockerfile et la configuration des serveurs, car elles ne sont pas toujours les mêmes. Je les compare ensuite aux dates de fin de support publiées par endoflife.date :

curl -s https://endoflife.date/api/php.json \
  | jq -r '.[:6][] | "\(.cycle)\tfin de support : \(.eol)"'

Une version en fin de vie n’est pas une opinion, c’est une date. Après elle, plus aucune faille n’est corrigée par l’éditeur. La migration devient obligatoire, et c’est souvent le plus gros poste du chiffrage, parce qu’elle entraîne celle du framework et d’une partie des bibliothèques.

4. Les dépendances : vulnérabilités, retard et licences

Je lance les outils standard de chaque écosystème, sur le fichier de verrouillage, qui décrit ce qui tourne réellement :

composer audit --locked
npm audit --omit=dev
osv-scanner scan source -r .

Une sortie type de composer audit :

Found 7 security vulnerability advisories affecting 4 packages:
+-------------------+----------------------------------------------+
| Package           | symfony/http-kernel                          |
| Severity          | high                                         |
| CVE               | CVE-XXXX-XXXXX                               |
| Affected versions | >=5.0.0,<5.4.20                              |
+-------------------+----------------------------------------------+

Ce qui trompe ici, c’est le nombre. Sept alertes peuvent valoir une demi-journée si ce sont des mises à jour mineures, ou plusieurs semaines si la correction n’existe que dans une version majeure que le reste du code ne supporte pas. Je croise donc avec le retard de versions :

composer outdated --direct --major-only
doctrine/orm        2.14.1  3.x  ...
symfony/framework-bundle  5.4.21  7.x  ...
guzzlehttp/guzzle   6.5.8   7.x  ...

Une dépendance avec deux versions majeures de retard, c’est un chantier en soi. Une dépendance dont le dernier commit date de plusieurs années, c’est une bibliothèque abandonnée à remplacer : je le vérifie sur son dépôt, pas sur sa page de documentation.

Les licences, ensuite. C’est le point que les acheteurs oublient, et celui que leurs avocats regretteront :

composer licenses --no-dev
npx license-checker --production --summary
├─ MIT: 612
├─ ISC: 84
├─ Apache-2.0: 31
├─ BSD-3-Clause: 18
├─ AGPL-3.0: 1
└─ UNKNOWN: 2

MIT, BSD ou Apache ne posent pas de difficulté. Une seule dépendance sous AGPL, en revanche, peut imposer de publier le code source du produit si celui-ci est fourni à des utilisateurs à travers le réseau, ce qui est exactement le cas d’un SaaS. Je ne tranche pas la question juridique : je la signale, avec le nom du paquet et l’endroit où il est utilisé, et l’avocat de l’acheteur décide. Les « UNKNOWN » se vérifient un par un.

Enfin, les secrets. Une clé d’API retirée du code reste dans l’historique git pour toujours :

gitleaks git --redact -v .
# versions plus anciennes : gitleaks detect --source . --redact -v
Finding:     STRIPE_SECRET_KEY=REDACTED
RuleID:      stripe-access-token
File:        .env.prod
Commit:      a1b2c3d
Date:        AAAA-MM-JJT10:42:00Z

Une clé commitée il y a plusieurs années et jamais changée est toujours valide. Le constat n’est pas « il y a un secret dans l’historique », mais « ces clés doivent être révoquées et régénérées avant la réalisation ». C’est rapide à corriger, et je ne laisse jamais passer ce point.

5. L’analyse statique : lire le niveau, pas le résultat

Une équipe qui me dit « on a PHPStan » ne m’a encore rien dit. PHPStan a des niveaux de 0 (très permissif) à 10, Psalm a des errorLevel de 8 (permissif) à 1 (strict). Je lis d’abord la configuration :

grep -E 'level' phpstan.neon phpstan.neon.dist 2>/dev/null
grep -c 'message:' phpstan-baseline.neon 2>/dev/null

La seconde commande est la plus instructive. Une baseline enregistre les erreurs existantes pour qu’elles ne bloquent plus la CI. Un niveau 8 avec 2 400 erreurs dans la baseline, c’est un niveau 8 de façade. Je compte aussi les suppressions dans le code :

grep -rn '@phpstan-ignore\|@psalm-suppress' src | wc -l
grep -rn 'eslint-disable' assets src --include='*.js' --include='*.ts' | wc -l

Puis je lance l’outil moi-même à un niveau intermédiaire, sans baseline, pour avoir une mesure comparable d’un projet à l’autre :

vendor/bin/phpstan analyse src --level=5 --no-progress --memory-limit=1G
npx eslint . --format compact | tail -3

Le nombre d’erreurs brut m’importe peu. Je regarde leur nature : des appels de méthodes sur des valeurs qui peuvent être nulles dans le module de paiement m’inquiètent davantage que mille annotations manquantes dans le back-office.

6. Les tests et la CI : ce qui protège vraiment une mise en production

Je compte d’abord les tests, puis je les lance sur mon clone, dans un conteneur :

find tests -name '*Test.php' | wc -l
XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-text --only-summary-for-coverage-text
Code Coverage Report Summary:
  Classes: 12.40% (38/306)
  Methods: 18.75% (420/2240)
  Lines:   21.30% (6100/28640)

Le pourcentage global trompe dans les deux sens. Un 60 % obtenu en testant les entités et les getters ne protège rien. Un 20 % concentré sur la facturation et l’authentification protège l’essentiel. Je regarde donc la couverture des dossiers critiques, pas la moyenne. Et je note si la suite de tests passe tout simplement : une suite qui échoue depuis des mois, c’est une suite que plus personne ne lance.

Puis je lis la CI ligne à ligne (.github/workflows/*.yml, .gitlab-ci.yml). Les signaux que je cherche :

  • les tests sont‑ils lancés à chaque demande de fusion, ou seulement à la main ?
  • une étape est‑elle neutralisée par continue‑on-error: true, allow_failure: true ou un || true en fin de commande ? Une CI toujours verte parce qu’elle ne peut pas échouer est pire qu’une absence de CI, car elle rassure ;
  • le déploiement est‑il automatisé, ou décrit dans un fichier texte que seule une personne sait suivre ?
  • les secrets de déploiement sont‑ils dans le gestionnaire de secrets de la CI, ou en clair dans le fichier ?
grep -rnE 'allow_failure|continue-on-error|\|\| true' .gitlab-ci.yml .github/ 2>/dev/null
gh run list --limit 20   # taux d'échec des dernières exécutions, si GitHub

Quand le déploiement est manuel, je demande à le voir faire, chronomètre en main. Une mise en production de deux heures, avec des étapes à ne pas oublier, se chiffre facilement : c’est le coût de son automatisation.

7. L’infrastructure et les sauvegardes : le seul test qui compte

Pour l’infrastructure, je vérifie d’abord la propriété : à qui appartiennent le compte d’hébergement, le nom de domaine, le compte du registre DNS, les comptes des services tiers (paiement, envoi d’e-mails, stockage). Tout doit être au nom de la société, pas d’un fondateur ou d’un ancien prestataire :

whois exemple.com | grep -iE 'registrant|registrar'
dig +short NS exemple.com

Je relève aussi qui a accès en écriture à la production, les comptes d’anciens collaborateurs encore actifs, la version du système des serveurs et l’existence d’une supervision. Une équipe qui ne sait pas dire quand a eu lieu le dernier incident n’a en général ni alerte ni procédure.

Les sauvegardes, ensuite. La phrase « on a des sauvegardes tous les jours » ne vaut rien tant qu’on n’a pas restauré. Je demande à l’équipe de le faire devant moi, sur une base vierge, hors production :

createdb restore_test
time pg_restore --no-owner --dbname=restore_test sauvegarde-du-jour.dump
psql restore_test -c "SELECT count(*) FROM invoice;"
# MySQL : time mysql restore_test < sauvegarde-du-jour.sql

Je note trois choses : est-ce que ça marche, combien de temps ça prend, et est-ce que les données sont complètes (je compare quelques comptages avec la production, lus par l’équipe). Les échecs typiques : une sauvegarde de la base sans les fichiers déposés par les utilisateurs, une sauvegarde chiffrée dont personne ne retrouve la clé, ou une restauration qui prend une journée alors que l’engagement de service promet quatre heures.

8. La grille de notation

Je note chaque domaine sur 10, avec une grille fixe, pour que deux audits soient comparables et que la note ne dépende pas de mon humeur. La note est toujours accompagnée d’une phrase qui la justifie.

DomaineCe que je mesure8 à 100 à 4
CodePoints chauds, taille des fichiers, analyse statique et baselineAnalyse statique stricte, baseline faible, modules découpésFichiers géants modifiés sans cesse, baseline de milliers d’erreurs
ArchitectureVersions, découpage, dépendances majeures, capacité à monter en chargeVersions maintenues, frontières clairesVersion en fin de vie, tout dépend de tout
SécuritéAudits de dépendances, secrets, accès, données personnellesAucune vulnérabilité élevée, secrets dans un coffreVulnérabilités critiques, clés valides dans l’historique
Tests et déploiementCouverture des parcours critiques, CI, automatisationTests sur chaque fusion, déploiement en une commandeAucun test, mise en production manuelle
Équipe et savoirBus factor, répartition des commits sur les modules critiquesAu moins deux personnes par module critiqueUne seule personne porte l’essentiel
DocumentationInstallation, déploiement, architecture, accèsUn nouveau développeur installe le projet seul en une journéeRien d’écrit, tout est dans une tête

Je fais un test simple pour la documentation : j’essaie d’installer le projet en suivant uniquement le README. Le temps que j’y passe, et les questions que je dois poser, sont la note.

9. Des constats aux jours de travail

Un rapport qui dit « la dette technique est importante » n’aide personne à négocier. Chaque constat devient un chantier, chaque chantier une fourchette en jours, avec ses hypothèses écrites. Par exemple :

ConstatChantierEstimationHypothèse
Langage en fin de vie, framework deux majeures en retardMigration du langage puis du framework25 à 30 joursPar étapes, avec des tests ajoutés sur les parcours touchés
Bus factor de unTransmission documentée à un second développeur10 joursLe développeur clé reste disponible pendant la transmission
Restauration jamais testéeProcédure de restauration écrite et testée3 joursLes sauvegardes existent et sont complètes
Déploiement manuelPipeline de déploiement automatisé8 joursHébergement actuel conservé

Je chiffre en jours, jamais en euros. La raison est simple : le même chantier ne coûte pas la même chose selon qui le fait. L’équipe interne de l’acheteur, un prestataire offshore, une agence parisienne ou l’équipe actuelle de la cible n’ont ni le même tarif journalier ni la même vitesse sur ce code. C’est à l’acheteur de valoriser ces jours au coût de l’équipe qui fera réellement le travail. Mon rôle est que le nombre de jours soit juste, et que ses hypothèses soient vérifiables.

Je donne toujours une fourchette, et l’écart dit quelque chose. Un écart étroit signifie que j’ai vu le code concerné. Un écart large signifie qu’une partie reste incertaine, et je dis laquelle. Je classe aussi chaque chantier par urgence : ce qui doit être réglé avant la réalisation, ce qui doit l’être dans les six mois, et ce qui peut attendre.

10. Des jours de travail aux clauses

La dernière partie du rapport, « Pour la transaction », traduit les constats dans le vocabulaire de ceux qui négocient. Je ne rédige pas les clauses, c’est le travail des avocats, mais je leur donne la matière technique précise. Quatre leviers reviennent presque toujours :

  • Le prix. Le total des jours de remise à niveau, valorisé par l’acheteur au tarif de son équipe, donne une base objective pour ajuster le prix. C’est plus solide qu’une impression générale sur la « qualité du code ».
  • La garantie. Ce que je ne peux pas vérifier complètement en huit jours se couvre par une garantie spécifique : licences des dépendances, cession des droits par les freelances et les agences qui ont écrit du code, conformité du traitement des données personnelles.
  • La condition suspensive. Ce qui est binaire et rapide se règle avant la réalisation de la cession : un test de restauration réussi, les comptes d’hébergement et le nom de domaine transférés au nom de la société, les clés exposées révoquées.
  • La fidélisation. Quand le savoir repose sur une personne, le risque ne se corrige pas avec du code. Un dispositif de fidélisation du développeur clé, signé avant la réalisation, lui laisse le temps de transmettre ce qu’il sait.

Le rapport se termine par un plan à 30, 60 et 90 jours, qui ordonne les chantiers après la signature. Vous pouvez voir la structure complète (synthèse, notes par domaine, risques, plan et transaction) dans l’exemple de rapport, construit sur des données fictives.

Une dette chiffrée en jours se négocie. Une dette découverte après la signature se subit.

Ce que j’en retiens

Aucune de ces commandes n’est secrète. git shortlog, composer audit, gitleaks ou pg_restore sont à la portée de n’importe quelle équipe. Ce qui fait la valeur d’une due diligence, c’est de savoir ce que leurs sorties cachent : un niveau PHPStan tenu par une baseline, une CI qui ne peut pas échouer, un bus factor masqué par des alias, une couverture de tests qui protège les getters et pas la facturation, une sauvegarde qui n’a jamais été restaurée.

Et c’est de garder en tête à qui s’adresse le rapport. Les développeurs ont besoin du détail en annexe. Le comité d’investissement a besoin de trois choses : combien de jours, quand, et ce qu’il faut inscrire dans le protocole.

En résumé : une due diligence technique se mène en lecture seule, les mesures avant les entretiens : historique git pour le savoir, audits de dépendances pour la sécurité et les licences, configuration de l’analyse statique et de la CI pour la qualité réelle, test de restauration pour les sauvegardes. Chaque constat devient une fourchette en jours, que l’acheteur valorise au coût de son équipe, puis un levier de négociation : prix, garantie, condition suspensive ou fidélisation.