QuarantineEventsV2: historial de descargas en macOS
Qué registra la base QuarantineEventsV2 de macOS: esquema de LSQuarantineEvent, tiempo absoluto de Mac, números de tipo, unión por UUID del xattr y límites.
TL;DR. QuarantineEventsV2 es una base SQLite por usuario, en ~/Library/Preferences/, donde macOS registra las descargas realizadas por las aplicaciones que participan en la cuarentena, como Safari, Chrome, Mail, Mensajes y AirDrop. Su única tabla, LSQuarantineEvent, indica cuándo (en tiempo absoluto de Mac), qué aplicación, desde qué URL y aporta un UUID de evento que también cierra el atributo com.apple.quarantine del archivo descargado. No guarda el nombre del archivo y nunca ve lo que se obtuvo con curl, wget o git.
Qué es QuarantineEventsV2
Cuando una aplicación que participa en la cuarentena guarda algo procedente de la red, macOS marca el archivo con el atributo de cuarentena y, en muchas de esas descargas, escribe un evento en la base QuarantineEventsV2 del usuario. El archivo está aquí, uno por cuenta de usuario:
~/Library/Preferences/com.apple.LaunchServices.QuarantineEventsV2
Las filas suelen sobrevivir al borrado del archivo descargado y no hay ninguna purga automática documentada. Por eso la base describe a menudo descargas cuyos archivos desaparecieron hace tiempo, y en la práctica funciona como un historial de descargas. Aun así, las filas se pueden borrar con sqlite3 y el archivo entero se puede eliminar.
El esquema de LSQuarantineEvent
Este es el esquema observado en un sistema macOS 26.6. Apple no documenta las columnas; las descripciones siguientes se basan en sus nombres y en los datos observados.
| Columna | Tipo | Contenido |
|---|---|---|
LSQuarantineEventIdentifier | TEXT, clave primaria, no nulo | UUID del evento; también es el último campo del atributo de cuarentena del archivo |
LSQuarantineTimeStamp | REAL | Hora del evento en tiempo absoluto de Mac |
LSQuarantineAgentBundleIdentifier | TEXT | Identificador de bundle de la aplicación que descargó |
LSQuarantineAgentName | TEXT | Nombre de la aplicación que descargó |
LSQuarantineDataURLString | TEXT | URL de los datos descargados |
LSQuarantineSenderName | TEXT | Nombre del remitente, cuando el elemento procede de una persona |
LSQuarantineSenderAddress | TEXT | Dirección del remitente, en el mismo caso |
LSQuarantineTypeNumber | INTEGER | Número de tipo del evento (ver más abajo) |
LSQuarantineOriginTitle | TEXT | Título de la página de origen |
LSQuarantineOriginURLString | TEXT | Página o ubicación desde la que empezó la descarga |
LSQuarantineOriginAlias | BLOB | Datos de alias binarios |
Hay dos índices: LSQuarantineEventIndex sobre el identificador y LSQuarantineTimeStampIndex sobre la marca de tiempo. En ese sistema la base usaba el modo de diario de rollback (journal_mode=delete) con un tamaño de página de 4096 bytes, así que es más probable encontrar un archivo -journal que un -wal, pero recoja ambos.
Marcas de tiempo: tiempo absoluto de Mac
LSQuarantineTimeStamp cuenta los segundos transcurridos desde el 2001-01-01 00:00:00 UTC, es decir, el tiempo absoluto de Mac. Sume 978307200 para obtener una marca de tiempo Unix. La columna se declara REAL, pero SQLite puede guardar en disco como entero un REAL cuyo valor es un número entero, una optimización de almacenamiento documentada. Un parser que solo acepte flotantes de 8 bytes leerá mal u omitirá esas filas; tiene que aceptar ambas codificaciones.
LSQuarantineTypeNumber
La cabecera pública de Apple LSQuarantine.h enumera seis constantes de tipo en este orden: WebDownload, OtherDownload, EmailAttachment, InstantMessageAttachment, CalendarEventAttachment, OtherAttachment. Asignarlas a los valores 0 a 5 en ese orden es la interpretación que se suele citar; Apple no la documenta.
| Valor | Significado citado | Observado en macOS 26.6 |
|---|---|---|
| 0 | WebDownload | Descargas de Chrome y Homebrew Cask |
| 1 | OtherDownload | |
| 2 | EmailAttachment | |
| 3 | InstantMessageAttachment | Mensajes |
| 4 | CalendarEventAttachment | |
| 5 | OtherAttachment | |
| 6 | No documentado | sharingd (AirDrop) |
| 7 | No documentado | Una fila de Safari |
Trate el número como un indicio y apóyese en las columnas del agente para saber qué aplicación intervino.
Observaciones en un sistema macOS 26.6 real
Lo siguiente procede de un único sistema de prueba. Son observaciones, no reglas, pero explican muchos resultados desconcertantes:
- Muchas filas, sobre todo de Chrome y Mensajes, no tenían ninguna columna de URL rellenada.
- Las descargas de Safari llevaban un UUID en su atributo de cuarentena pero no tenían fila correspondiente en la base en los últimos meses.
- Algunos valores del atributo eran
0081;00000000;;: sin hora, sin agente, sin UUID. - En el atributo, los nombres de agente escapan los espacios como
\x20, por ejemploMicrosoft\x20Word. - Algunos UUID estaban en minúsculas.
La lección: un UUID sin correspondencia o una URL vacía son habituales y no resultan sospechosos por sí solos.
Qué prueba una fila y qué no
Una fila viva muestra que una aplicación que participa en la cuarentena registró un evento de descarga o de adjunto, para esta cuenta de usuario, a esa hora, con esas URL y ese agente. Unida al atributo de un archivo, vincula un archivo en disco con un origen.
No demuestra que el archivo siga existiendo, que se abriera o ejecutara, ni quién estaba ante el teclado: una sesión robada produce las mismas filas que el usuario legítimo. No nombra el archivo local. Y la ausencia de una fila es una prueba débil: las descargas hechas con herramientas que no participan (curl, wget, scp, rsync, git) nunca aparecen, las filas se pueden borrar con sqlite3 y el propio archivo se puede eliminar.
Consultar la base con sqlite3
Trabaje sobre una copia, nunca sobre el original (abrir una base puede modificarla; vea recuperar registros borrados). Esta consulta lista los eventos por orden cronológico con una fecha UTC:
SELECT datetime(LSQuarantineTimeStamp + 978307200, 'unixepoch') AS event_utc,
LSQuarantineAgentBundleIdentifier, LSQuarantineDataURLString,
LSQuarantineOriginURLString, LSQuarantineEventIdentifier
FROM LSQuarantineEvent ORDER BY LSQuarantineTimeStamp;
Unir eventos y archivos mediante el UUID del xattr
La base conoce las URL pero no los archivos; el archivo conoce su evento pero no siempre su URL. El vínculo es el UUID. Un atributo de cuarentena tiene este aspecto:
0083;66f7c2a1;Safari;6E4C2B1A-...
El último campo coincide con LSQuarantineEventIdentifier. Compare sin distinguir mayúsculas y minúsculas, porque algunos UUID se guardan en minúsculas. Un evento puede corresponder a varios archivos: la Utilidad de Archivo copia el valor de cuarentena del archivo comprimido, UUID incluido, en cada archivo que extrae. El campo de flags y las demás partes del valor se explican en los flags de com.apple.quarantine, y el atributo de URL que ponen los navegadores en kMDItemWhereFroms frente a la cuarentena.
Cómo lee la base Quarantine Parser
Quarantine Parser se ejecuta por completo en su navegador (Rust compilado a WebAssembly en un Web Worker); no se sube nada. Usa su propio lector SQLite de solo lectura en lugar de una biblioteca SQLite, lo que importa en tres aspectos:
- WAL. Un archivo
-walse aplica como lo hace SQLite: se verifican las sumas de comprobación de la cabecera y de los frames y las sales, y solo se usan los frames confirmados. También se lee el archivo-journalasociado. - Copias parciales. Las copias truncadas, parciales o bloqueadas se leen hasta donde llegan, en lugar de fallar en el primer error.
- Recuperación. Las filas que ya no están vivas se extraen mediante carving del espacio libre de las páginas de tabla, de las páginas de la freelist, de copias antiguas de páginas, de frames WAL sustituidos o no confirmados y de páginas del diario de rollback. Vea recuperar registros borrados de QuarantineEventsV2.
Puede arrastrar a la vez las bases de varios usuarios, incluidas las copias lsquarantine_<user> de Aftermath, junto con listados de xattr. Las vistas Cronología, Agentes, Dominios y Archivos muestran entonces cada evento con los archivos que llevan su UUID, y la vista Hallazgos enumera pistas, no veredictos.
Lecturas relacionadas
- Recoger los archivos: UAC, Aftermath y Velociraptor
- Un caso completo: investigación de descargas en macOS paso a paso
- Qué ocurre cuando Gatekeeper rechaza un elemento: .LastGKReject como prueba de Gatekeeper
- Una chuleta de una página: eventos de cuarentena en macforensics.app
Preguntas frecuentes
¿Dónde está la base QuarantineEventsV2 en macOS?
Cada usuario tiene una en ~/Library/Preferences/com.apple.LaunchServices.QuarantineEventsV2. Es una base SQLite con una sola tabla, LSQuarantineEvent. Cópiela junto con cualquier archivo -wal o -journal que haya a su lado.
¿Registra QuarantineEventsV2 el nombre del archivo descargado?
No. Guarda URL, la aplicación que descargó, una marca de tiempo y un UUID de evento, pero no la ruta local ni el nombre del archivo. Para vincular un evento con un archivo, compare el UUID que aparece al final del atributo com.apple.quarantine del archivo.
¿Cómo convierto LSQuarantineTimeStamp en una fecha?
Es tiempo absoluto de Mac: segundos desde el 2001-01-01 00:00:00 UTC. Sume 978307200 para obtener tiempo Unix, por ejemplo con datetime(LSQuarantineTimeStamp + 978307200, 'unixepoch') en sqlite3.
¿Una QuarantineEventsV2 vacía significa que no se descargó nada?
No. Herramientas como curl, wget, scp, rsync y git no participan en la cuarentena, las filas se pueden borrar con sqlite3 y el archivo entero se puede eliminar. La ausencia es una prueba débil.