Récupérer les enregistrements QuarantineEventsV2 supprimés
Comment des lignes QuarantineEventsV2 supprimées survivent dans les freeblocks, la freelist, le WAL et les journaux SQLite, et ce que prouve leur récupération.
TL;DR. Supprimer une ligne de QuarantineEventsV2 avec sqlite3 ne l’efface pas. L’enregistrement reste généralement dans le fichier sous forme d’espace libre dans la page de table, et d’anciennes versions de pages peuvent aussi se trouver dans les pages de la freelist, le WAL et le journal de rollback. Quarantine Parser extrait par carving les enregistrements LSQuarantineEvent de tous ces emplacements en s’ancrant sur les serial types de l’enregistrement, qui survivent quand les 4 premiers octets d’une cellule libérée sont écrasés. La récupération dépend de secure_delete et des écritures ultérieures, et l’absence de lignes récupérées ne prouve rien.
Comment les lignes de quarantaine disparaissent
La base QuarantineEventsV2 n’a pas de purge automatique documentée ; les lignes survivent généralement aux fichiers qu’elles décrivent. Elles disparaissent de deux façons :
- Suppression de lignes. Toute personne ayant accès aux fichiers de l’utilisateur peut exécuter un
DELETEsurLSQuarantineEventavec sqlite3, pour retirer une ligne compromettante ou tout l’historique. Un effacement de routine de l’historique des téléchargements peut aussi supprimer des lignes. - Suppression du fichier. La base entière, avec ses compagnons, peut être supprimée. Ce qui reste relève alors de la récupération au niveau du système de fichiers, pas du carving SQLite.
C’est dans le premier cas que le carving aide, car par défaut SQLite n’efface pas ce qu’il supprime.
Où survivent les enregistrements supprimés
Espace libre dans les pages de table
SQLite stocke les lignes sous forme de cellules dans des pages de taille fixe (4096 octets sur le système macOS 26.6 observé). Quand une ligne est supprimée, sa cellule n’est pas remise à zéro par défaut : l’espace rejoint une chaîne de freeblocks dans la page, et les anciens octets restent jusqu’à ce qu’une insertion ultérieure les réutilise. C’est l’endroit le plus courant pour retrouver un événement de quarantaine supprimé.
Pages de la freelist
Quand une page ne contient plus aucune cellule active, SQLite la place dans la freelist pour la réutiliser. Son ancien contenu peut subsister jusqu’à ce que la page soit réattribuée.
Anciennes copies de pages : fichier principal ou WAL
Avec un journal d’écriture anticipée (WAL), les nouvelles versions des pages sont d’abord écrites dans le fichier -wal et n’atteignent le fichier principal qu’au checkpoint. D’ici là, le fichier principal contient l’ancienne version de chaque page modifiée et le WAL la nouvelle. Une ligne supprimée dans la version du WAL peut donc être encore active dans la copie du fichier principal, et inversement après un checkpoint.
Frames WAL remplacées et non validées
Un WAL peut contenir plusieurs frames pour une même page, chacune étant une version plus récente. Les frames remplacées conservent des états antérieurs de la page, et les frames écrites après le dernier commit n’ont jamais été validées. SQLite ignore les unes comme les autres quand il lit la base ; un outil de carving n’y est pas obligé.
Pages du journal de rollback
En mode journal de rollback, celui qu’utilisait le système macOS 26.6 observé (journal_mode=delete), SQLite copie la version d’origine de chaque page dans le fichier -journal avant de la modifier. En mode DELETE, le journal est supprimé au commit : un -journal à côté de la base signifie donc généralement qu’une transaction était en cours ou a été interrompue au moment de la copie ; ses pages peuvent contenir les lignes telles qu’elles étaient avant la modification. En mode PERSIST, SQLite conserve le fichier journal et se contente d’invalider son en-tête : d’anciennes images de pages peuvent donc y subsister elles aussi.
secure_delete et écritures ultérieures
SQLite dispose d’un réglage secure_delete qui écrase le contenu supprimé. Lorsqu’il est actif, les cellules libérées sont remises à zéro et le carving de l’espace libre ne trouve rien. La configuration qu’Apple applique à cette base n’est pas documentée : ne supposez rien dans un sens ni dans l’autre. Indépendamment de ce réglage, toute insertion ultérieure peut réutiliser l’espace libéré, et une base très active le recycle plus vite. Ces deux raisons expliquent pourquoi la récupération reste opportuniste.
Pourquoi le carving s’ancre sur les serial types
Un enregistrement SQLite commence par la longueur de sa charge utile et son rowid, suivis d’un en-tête qui liste un serial type par colonne, puis des valeurs des colonnes. Quand une cellule est libérée, SQLite écrit l’en-tête du freeblock (le décalage du freeblock suivant et la taille de celui-ci) par-dessus les 4 premiers octets de la cellule. Le début de la cellule est détruit, mais les serial types survivent.
Pour LSQuarantineEvent, ces serial types forment un motif reconnaissable : onze colonnes, surtout du texte, un REAL pour l’horodatage (ou un entier quand la valeur est entière, une optimisation de stockage appliquée par SQLite), un entier pour le numéro de type et un blob ou NULL pour l’alias. Quarantine Parser recherche ce motif dans l’espace libre et décode les valeurs de colonnes qui le suivent pour reconstruire la ligne.
Enregistrements partiels
Un enregistrement extrait peut être partiel : des écritures ultérieures ont pu écraser une partie de la cellule libérée, ou les octets nécessaires aux dernières colonnes ont pu disparaître. Ne considérez un champ comme fiable que s’il se décode proprement. L’identifiant et une URL suffisent souvent pour faire la jointure et pivoter. Comparez les enregistrements récupérés aux lignes actives : une ancienne copie de page peut aussi contenir une ligne toujours active, alors vérifiez les doublons avant de compter les suppressions.
Ce que prouvent les lignes récupérées, et ce qu’elles ne prouvent pas
Une ligne récupérée montre que cet événement a existé dans la base de cet utilisateur et qu’il n’est plus actif. C’est une piste, pas un verdict. Elle ne dit ni quand la ligne a été supprimée, ni par qui ; SQLite ne l’enregistre pas. Elle ne distingue pas un DELETE délibéré d’un effacement de routine. C’est pourquoi la vue Constats la formule comme une « suppression ou un effacement possible ». Le contexte tranche : une seule ligne manquante dans un historique régulier, juste après un téléchargement suspect, ne se lit pas comme un historique vidé en bloc.
Et à l’inverse : quand rien n’est récupéré, rien n’est prouvé. L’espace libéré a pu être réutilisé, ou secure_delete a pu le remettre à zéro.
Préserver la preuve
- Copiez la base avec ses fichiers
-walet-journal(cp -pavec un*final, comme dans le guide de collecte). - Calculez les empreintes des copies avant toute analyse.
- N’ouvrez pas l’original avec sqlite3. L’ouverture d’une base peut reporter le WAL dans le fichier principal par un checkpoint ou annuler un journal chaud, ce qui écrase précisément les pages que vous voulez exploiter. Même sur une copie, n’utilisez sqlite3 que sur une seconde copie.
- Préférez les fichiers bruts aux sorties analysées : un export analysé ne contient que les lignes actives renvoyées par la requête.
Quarantine Parser n’écrit jamais dans les fichiers : il utilise son propre lecteur SQLite en lecture seule dans votre navigateur, applique les frames WAL validées comme le fait SQLite, et extrait le reste par carving.
Exemple : FIN-MBP-03
Le cas synthétique derrière « Try a sample » sur la page d’accueil montre toute la chaîne. Sur l’ordinateur portable Mac FIN-MBP-03, utilisateur dana.whitlock, le 2026-09-14 :
- À 10:07:12, Safari télécharge
https://files.example/s/8f3k2/tools.zip(originehttps://files.example/s/8f3k2). L’Utilitaire d’archive extraittools/README.txt,tools/updater.shettools/com.example.updater.plist, chacun avec la valeur de quarantaine et l’UUID de l’archive. - Une copie de
com.example.updater.plistse trouve dans~/Library/LaunchAgents/et porte toujours cette valeur. - Vers 10:48, la ligne files.example est supprimée de la base avec sqlite3.
Dans la table active, il n’y a plus aucun événement files.example, et plusieurs UUID d’attributs n’ont plus de ligne dans la base. Quarantine Parser récupère la ligne dans l’espace libre de la page de table et la relie aux cinq fichiers qui portent son UUID, copie dans LaunchAgents comprise. La chronologie vient du scénario : dans une affaire réelle, la base ne vous dirait pas quand la suppression a eu lieu, pas plus qu’elle ne montrerait la session Terminal utilisée pour lancer sqlite3. C’est dans les unified logs, TCC.db et FSEvents qu’il faut chercher.
Pour le versant attribut de la jointure, voir les flags de com.apple.quarantine ; pour l’enquête complète, l’enquête sur des téléchargements macOS pas à pas ; pour une fiche de référence, macforensics.app.
Questions fréquentes
Peut-on récupérer des enregistrements QuarantineEventsV2 supprimés ?
Souvent, mais pas toujours. Une ligne supprimée peut survivre dans l’espace libre des pages de table, dans les pages de la freelist, dans d’anciennes copies de pages du WAL ou du fichier principal, et dans les pages du journal de rollback. Le réglage secure_delete de SQLite et les écritures ultérieures peuvent la détruire.
Un enregistrement récupéré prouve-t-il que quelqu’un a manipulé la base ?
Non. Il prouve que la ligne a existé dans cette base et qu’elle n’est plus active. Une suppression délibérée avec sqlite3 et un effacement de routine peuvent se ressembler ; la chronologie, les autres artefacts et les fichiers portant l’UUID aident à trancher.
Si rien n’est récupéré, aucune ligne n’a-t-elle été supprimée ?
Non. L’espace libéré est réutilisé par les écritures suivantes et secure_delete écrase le contenu supprimé : l’absence de lignes récupérées ne prouve rien.
Comment éviter de détruire les enregistrements supprimés pendant la collecte ?
Copiez la base avec ses fichiers -wal et -journal, calculez les empreintes des copies et travaillez sur une copie de la copie. N’ouvrez pas l’original avec sqlite3 : l’ouverture peut déclencher un checkpoint du WAL ou le rollback d’un journal et écraser l’espace libre.