QuarantineEventsV2 : l’historique des téléchargements macOS
Ce qu’enregistre la base macOS QuarantineEventsV2 : schéma LSQuarantineEvent, temps absolu Mac, numéros de type, jointure par UUID de l’xattr et limites.
TL;DR. QuarantineEventsV2 est une base SQLite propre à chaque utilisateur, dans ~/Library/Preferences/, où macOS consigne les téléchargements effectués par les applications qui participent à la quarantaine : Safari, Chrome, Mail, Messages, AirDrop, entre autres. Son unique table, LSQuarantineEvent, indique quand (en temps absolu Mac), quelle application, depuis quelle URL, et fournit un UUID d’événement que l’on retrouve à la fin de l’attribut com.apple.quarantine du fichier téléchargé. Elle ne stocke pas le nom du fichier et ne voit jamais ce que curl, wget ou git ont récupéré.
Qu’est-ce que QuarantineEventsV2 ?
Lorsqu’une application qui participe à la quarantaine enregistre un élément venu du réseau, macOS appose l’attribut de quarantaine sur le fichier et, pour bon nombre de ces téléchargements, écrit un événement dans la base QuarantineEventsV2 de l’utilisateur. Le fichier se trouve ici, un par compte utilisateur :
~/Library/Preferences/com.apple.LaunchServices.QuarantineEventsV2
Les lignes survivent généralement à la suppression du fichier téléchargé, et aucune purge automatique n’est documentée. La base décrit donc souvent des téléchargements dont les fichiers ont disparu depuis longtemps : c’est ce qui en fait, en pratique, un historique des téléchargements. Des lignes peuvent néanmoins être supprimées avec sqlite3, et le fichier entier peut être effacé.
Le schéma de LSQuarantineEvent
Voici le schéma observé sur un système macOS 26.6. Apple ne documente pas les colonnes ; les descriptions ci-dessous s’appuient sur leurs noms et sur les données observées.
| Colonne | Type | Contenu |
|---|---|---|
LSQuarantineEventIdentifier | TEXT, clé primaire, non nul | UUID de l’événement ; aussi dernier champ de l’attribut de quarantaine du fichier |
LSQuarantineTimeStamp | REAL | Heure de l’événement en temps absolu Mac |
LSQuarantineAgentBundleIdentifier | TEXT | Identifiant de bundle de l’application qui a téléchargé |
LSQuarantineAgentName | TEXT | Nom de l’application qui a téléchargé |
LSQuarantineDataURLString | TEXT | URL des données téléchargées |
LSQuarantineSenderName | TEXT | Nom de l’expéditeur, quand l’élément vient d’une personne |
LSQuarantineSenderAddress | TEXT | Adresse de l’expéditeur, dans le même cas |
LSQuarantineTypeNumber | INTEGER | Numéro de type de l’événement (voir plus bas) |
LSQuarantineOriginTitle | TEXT | Titre de la page d’origine |
LSQuarantineOriginURLString | TEXT | Page ou emplacement d’où est parti le téléchargement |
LSQuarantineOriginAlias | BLOB | Données d’alias binaires |
Deux index existent : LSQuarantineEventIndex sur l’identifiant et LSQuarantineTimeStampIndex sur l’horodatage. Sur ce système, la base utilisait le mode journal de rollback (journal_mode=delete) avec une taille de page de 4096 octets : attendez-vous à trouver un fichier -journal plus souvent qu’un -wal, mais collectez les deux.
Horodatages : le temps absolu Mac
LSQuarantineTimeStamp compte les secondes écoulées depuis le 2001-01-01 00:00:00 UTC, c’est-à-dire le temps absolu Mac. Ajoutez 978307200 pour obtenir un horodatage Unix. La colonne est déclarée REAL, mais SQLite peut stocker sur disque un REAL dont la valeur est entière sous forme d’entier, une optimisation de stockage documentée. Un parseur qui n’accepte que des flottants sur 8 octets lira mal ou ignorera ces lignes ; il doit accepter les deux encodages.
LSQuarantineTypeNumber
L’en-tête public d’Apple LSQuarantine.h liste six constantes de type dans cet ordre : WebDownload, OtherDownload, EmailAttachment, InstantMessageAttachment, CalendarEventAttachment, OtherAttachment. Les associer aux valeurs 0 à 5 dans cet ordre est l’interprétation couramment rapportée ; elle n’est pas documentée par Apple.
| Valeur | Signification rapportée | Observé sur macOS 26.6 |
|---|---|---|
| 0 | WebDownload | Téléchargements Chrome et Homebrew Cask |
| 1 | OtherDownload | |
| 2 | EmailAttachment | |
| 3 | InstantMessageAttachment | Messages |
| 4 | CalendarEventAttachment | |
| 5 | OtherAttachment | |
| 6 | Non documentée | sharingd (AirDrop) |
| 7 | Non documentée | Une ligne Safari |
Considérez ce numéro comme un indice et fiez-vous aux colonnes d’agent pour savoir quelle application était en jeu.
Observations sur un système macOS 26.6 réel
Les points suivants proviennent d’un seul système de test. Ce sont des observations, pas des règles, mais elles expliquent bien des résultats déroutants :
- Beaucoup de lignes, notamment de Chrome et de Messages, n’avaient aucune colonne d’URL renseignée.
- Les téléchargements Safari portaient un UUID dans leur attribut de quarantaine mais n’avaient aucune ligne correspondante dans la base ces derniers mois.
- Certaines valeurs d’attribut étaient
0081;00000000;;: ni heure, ni agent, ni UUID. - Dans l’attribut, les noms d’agent échappent les espaces en
\x20, par exempleMicrosoft\x20Word. - Quelques UUID étaient en minuscules.
La leçon : un UUID sans correspondance ou une URL vide sont fréquents et ne sont pas suspects en eux-mêmes.
Ce qu’une ligne prouve, et ce qu’elle ne prouve pas
Une ligne active montre qu’une application participant à la quarantaine a enregistré un événement de téléchargement ou de pièce jointe, pour ce compte utilisateur, à cette heure, avec ces URL et cet agent. Rapprochée de l’attribut d’un fichier, elle relie un fichier sur disque à une source.
Elle ne montre pas que le fichier existe encore, qu’il a été ouvert ou exécuté, ni qui était au clavier : une session volée produit les mêmes lignes que l’utilisateur légitime. Elle ne nomme pas le fichier local. Et l’absence de ligne est une preuve faible : les téléchargements faits par des outils qui ne participent pas à la quarantaine (curl, wget, scp, rsync, git) n’y figurent jamais, des lignes peuvent être supprimées avec sqlite3 et le fichier lui-même peut être effacé.
Interroger la base avec sqlite3
Travaillez sur une copie, jamais sur l’original (ouvrir une base peut la modifier ; voir récupérer les enregistrements supprimés). Cette requête liste les événements dans l’ordre chronologique avec une date UTC :
SELECT datetime(LSQuarantineTimeStamp + 978307200, 'unixepoch') AS event_utc,
LSQuarantineAgentBundleIdentifier, LSQuarantineDataURLString,
LSQuarantineOriginURLString, LSQuarantineEventIdentifier
FROM LSQuarantineEvent ORDER BY LSQuarantineTimeStamp;
Relier les événements aux fichiers par l’UUID de l’xattr
La base connaît les URL mais pas les fichiers ; le fichier connaît son événement mais pas toujours son URL. Le lien, c’est l’UUID. Un attribut de quarantaine ressemble à ceci :
0083;66f7c2a1;Safari;6E4C2B1A-...
Le dernier champ correspond à LSQuarantineEventIdentifier. Comparez sans tenir compte de la casse, puisque certains UUID sont stockés en minuscules. Un événement peut correspondre à plusieurs fichiers : l’Utilitaire d’archive recopie la valeur de quarantaine de l’archive, UUID compris, sur chaque fichier qu’il extrait. Le champ des flags et les autres parties de la valeur sont détaillés dans les flags de com.apple.quarantine, et l’attribut d’URL posé par les navigateurs dans kMDItemWhereFroms ou quarantaine.
Comment Quarantine Parser lit la base
Quarantine Parser fonctionne entièrement dans votre navigateur (Rust compilé en WebAssembly dans un Web Worker) ; rien n’est envoyé. Il utilise son propre lecteur SQLite en lecture seule plutôt qu’une bibliothèque SQLite, ce qui compte à trois égards :
- WAL. Un fichier
-walest appliqué comme le fait SQLite : les sommes de contrôle de l’en-tête et des frames ainsi que les sels sont vérifiés, et seules les frames validées sont utilisées. Un fichier-journalcompagnon est lu aussi. - Copies partielles. Les copies tronquées, partielles ou verrouillées sont lues aussi loin que possible, au lieu d’échouer à la première erreur.
- Récupération. Les lignes qui ne sont plus actives sont extraites par carving de l’espace libre des pages de table, des pages de la freelist, des anciennes copies de pages, des frames WAL remplacées ou non validées et des pages du journal de rollback. Voir récupérer les enregistrements QuarantineEventsV2 supprimés.
Vous pouvez déposer les bases de plusieurs utilisateurs à la fois, y compris les copies lsquarantine_<user> d’Aftermath, avec des listings xattr. Les vues Chronologie, Agents, Domaines et Fichiers affichent alors chaque événement avec les fichiers qui portent son UUID, et la vue Constats liste des pistes, pas des verdicts.
Pour aller plus loin
- Collecter les fichiers : UAC, Aftermath et Velociraptor
- Un cas complet : enquête sur des téléchargements macOS pas à pas
- Ce qui se passe quand Gatekeeper refuse un élément : .LastGKReject comme preuve Gatekeeper
- Un aide-mémoire d’une page : les événements de quarantaine sur macforensics.app
Questions fréquentes
Où se trouve la base QuarantineEventsV2 sur macOS ?
Chaque utilisateur en possède une dans ~/Library/Preferences/com.apple.LaunchServices.QuarantineEventsV2. C’est une base SQLite qui ne contient qu’une table, LSQuarantineEvent. Copiez-la avec les éventuels fichiers -wal ou -journal situés à côté.
QuarantineEventsV2 enregistre-t-elle le nom du fichier téléchargé ?
Non. Elle stocke des URL, l’application qui a téléchargé, un horodatage et un UUID d’événement, mais ni le chemin local ni le nom du fichier. Pour relier un événement à un fichier, rapprochez l’UUID situé à la fin de l’attribut com.apple.quarantine du fichier.
Comment convertir LSQuarantineTimeStamp en date ?
C’est du temps absolu Mac : des secondes depuis le 2001-01-01 00:00:00 UTC. Ajoutez 978307200 pour obtenir un temps Unix, par exemple avec datetime(LSQuarantineTimeStamp + 978307200, 'unixepoch') dans sqlite3.
Une base QuarantineEventsV2 vide signifie-t-elle qu’aucun téléchargement n’a eu lieu ?
Non. Des outils comme curl, wget, scp, rsync et git ne participent pas à la quarantaine, des lignes peuvent être supprimées avec sqlite3 et le fichier entier peut être effacé. L’absence est une preuve faible.