
Análisis forense de memoria Linux sin dependencias, aprovechando BTF y kallsyms integrados en el kernel para un análisis de memoria consciente del tipo sin información de depuración externa.
mquire, un juego de palabras entre memory e inquire, es una herramienta de consulta de memoria inspirada en osquery.
mquire puede analizar instantáneas de memoria del kernel de Linux sin requerir símbolos de depuración externos.
Todo lo necesario para el análisis ya está incluido en el propio volcado de memoria. Esto significa que puede analizar:
Requisitos de versión del kernel:
scripts/kallsyms.c)mquire analiza la memoria del kernel leyendo dos tipos de información que están incrustados en los kernels modernos de Linux:
/proc/kallsyms)Al combinar la información de tipo con las ubicaciones de los símbolos, mquire puede encontrar y leer estructuras de datos complejas del kernel como:
Esto permite extraer archivos directamente de la caché de archivos del kernel, incluso si se han eliminado del disco.
El escáner de Kallsyms depende del formato de datos de scripts/kallsyms.c en el código fuente del kernel. Si las versiones futuras del kernel cambian este formato, es posible que sea necesario actualizar las heurísticas del escáner.
mquire proporciona tablas SQL para consultar diferentes aspectos del sistema o el estado de la propia herramienta.
mquire no es una base de datos. Cada consulta reconstruye las estructuras de datos del kernel escaneando la memoria y siguiendo punteros. No hay índices precalculados ni resultados en caché: cada acceso a una tabla es un recorrido de los datos del kernel. Use
AS MATERIALIZEDpara evitar escaneos redundantes (consulte Optimización de consultas) y proporcione restricciones comotaskal consultar tablas por proceso comotask_open_filesymemory_mappingspara limitar el escaneo a un solo proceso.
Principio de diseño: direcciones virtuales como claves de unión. Las tablas usan
virtual_address(la dirección del kernel de la estructura de datos subyacente) como la clave de unión canónica: nopidni otros identificadores visibles para el usuario. Esto es intencional, porque el mismo PID puede aparecer varias veces en diferentes fuentes de descubrimiento y tareas raíz, mientras que una dirección virtual identifica de forma única un objeto del kernel específico. Tanto las tablas SQL como la API subyacenteLinuxOperatingSystemse construyen alrededor de esta convención.
/proc/kallsyms)dmesg)task para un análisis dirigido, o consulte todas las tareas a la vez)task para un análisis dirigido, o consulte todas las tareas a la vez)task, así que únala contra tasks/processes (por ejemplo, JOIN task_capabilities c ON c.task = p.virtual_address).task.mem de un objeto de módulo del kernel. Requiere una restricción kernel_module, así que únala contra kernel_modules (por ejemplo, JOIN kernel_module_mem_entries r ON r.kernel_module = m.virtual_address).struct ftrace_ops, recorridos desde el símbolo ftrace_ops_list por defecto. Restrinja virtual_address para leer un solo nodo, o start_vaddr (opcionalmente acotado por end_vaddr) para recorrer desde un nodo arbitrario.mquire proporciona tres comandos principales:
mquire shell: Inicia un shell SQL interactivo para consultar instantáneas de memoriamquire query: Ejecuta una sola consulta SQL y muestra los resultados (admite formato JSON o tabla)mquire command: Ejecuta comandos personalizados en instantáneas de memoria (por ejemplo, .task_tree, .system_version, .dump)mquire proporciona comandos especiales con prefijo de punto (.) para distinguirlos de las consultas SQL.
Estos comandos funcionan en el shell interactivo y con mquire query:
.tables: Lista todas las tablas disponibles.schema: Muestra el esquema de todas las tablas.schema <tabla>: Muestra el esquema de una tabla específica.commands: Lista todos los comandos personalizados disponibles.exit: Sale del shell interactivo (solo shell)Estos comandos funcionan en el shell interactivo y con mquire command:
Use --help con cualquier comando para ver las opciones disponibles y la información de uso. Por ejemplo: .task_tree --help
.system_versionMuestra la información de la versión del sistema operativo.
Este es un comando de conveniencia equivalente a SELECT * FROM os_version, pero con salida formateada.
.task_treeMuestra un árbol jerárquico de procesos y subprocesos en ejecución, similar al comando pstree en Linux.
Opciones:
--show-threads: Incluye subprocesos además de procesos. Cuando está habilitado, muestra tanto TGID como TID para cada entrada.--use-real-parent: Usa el campo real_parent en lugar de parent para construir la estructura del árbol. El campo real_parent muestra el proceso padre original antes de cualquier reasignación de padre (útil para rastrear cadenas de creación de procesos incluso después de que los procesos padre hayan salido).Notas:
[TGID TID] cuando se muestran subprocesos, o [TGID] cuando los subprocesos están ocultos. TGID (Thread Group ID) es lo que comúnmente se llama PID. Para subprocesos principales (donde TGID == TID), ambos valores serán iguales..carveExtrae una región de memoria virtual al disco. Este comando extrae contenido de memoria sin procesar de un rango de direcciones virtuales específico utilizando una tabla de páginas determinada, útil para extraer memoria de procesos, contenido del heap u otras regiones de memoria.
Argumentos:
ROOT_PAGE_TABLE: La dirección física de la tabla de páginas raíz (cadena hexadecimal con prefijo opcional 0x). Esto determina el espacio de direcciones a utilizar para la traducción.VIRTUAL_ADDRESS: La dirección virtual desde la que comenzar a extraer (cadena hexadecimal con prefijo opcional 0x).SIZE: Número de bytes a extraer.DESTINATION_PATH: Ruta del archivo de salida donde se escribirá la memoria extraída.Notas:
.dumpExtrae archivos de la caché de archivos del kernel para recuperar archivos directamente desde la memoria. Este comando itera a través de todas las tareas y sus descriptores de archivo abiertos, extrayendo el contenido de los archivos de la caché de páginas.
Argumentos:
OUTPUT: Directorio de salida para los archivos extraídos. Los archivos se organizan por TGID (por ejemplo, tgid_1234/ruta/al/archivo).Notas:
mquire está diseñado para:
Hay paquetes precompilados disponibles como artefactos de las ejecuciones de CI. Puede descargarlos desde la pestaña Actions seleccionando una ejecución de workflow exitosa y descargando los artefactos. Los siguientes formatos de paquete están disponibles:
.deb para Debian/Ubuntu.rpm para Fedora/RHEL/CentOS.tar.gz genérico para Linuxmquire está escrito en Rust. Para compilarlo:```bash
git clone https://github.com/trailofbits/mquire cd mquire
cargo build --release
## Formatos de instantáneas compatibles
mquire admite los siguientes formatos de instantáneas de memoria, detectados por extensión de archivo:
| Formato | Extensión | Descripción |
|--------|-----------|-------------|
| Raw | `.raw` | Volcado de memoria física plana (copia byte a byte del espacio de direcciones físicas) |
| LiME | `.lime` | Formato [Linux Memory Extractor](https://github.com/504ensicsLabs/LiME) con encabezados de rango de direcciones |
| ELF core | `.elf` | Volcado de núcleo ELF con segmentos PT_LOAD (como se produce con `virsh dump` o QEMU `dump-guest-memory`) |
## Adquisición de una instantánea de memoria
### Usando AVML (recomendado)
Recomendamos [AVML](https://github.com/microsoft/avml) para adquirir instantáneas de memoria de sistemas Linux en vivo. [LiME](https://github.com/504ensicsLabs/LiME) se sugería anteriormente pero ya no se mantiene activamente.```bash
sudo avml output.lime
Importante: No uses
--compressal adquirir instantáneas para mquire. mquire no admite instantáneas AVML comprimidas. Si tienes una instantánea comprimida, usaavml-convertpara descomprimirla primero:avml-convert compressed.lime uncompressed.lime
Consulta la documentación de AVML para opciones adicionales.
Para máquinas virtuales libvirt/KVM, usa virsh dump para producir un ELF core dump:```bash
virsh -c qemu:///system dump output.elf --memory-only --format elf
La VM se pausa durante el volcado y se reanuda después. Agregue `--live` para evitar la pausa, a costa de la consistencia de la instantánea.
## Primeros pasos
Una vez que tenga una instantánea de memoria, puede interactuar con ella usando consultas SQL y comandos personalizados. mquire proporciona tres formas de interactuar con las instantáneas:
### Shell interactivo
Inicie un shell SQL interactivo:```bash
mquire shell /path/to/memory.raw
Esto abre un prompt donde se pueden ejecutar tanto consultas SQL como comandos de forma interactiva:```bash mquire> .tables # List all available tables mquire> .schema tasks # Show schema for a specific table mquire> SELECT * FROM tasks; # Run SQL queries mquire> .task_tree --show-threads # Run custom commands mquire> .exit # Exit the shell
### Consultas SQL únicas
Ejecuta una sola consulta SQL o comando integrado desde la línea de comandos:```bash
# Output as JSON (default)
mquire query /path/to/memory.raw "SELECT * FROM os_version"
# Output as table format
mquire query /path/to/memory.raw "SELECT * FROM tasks" --format table
# Built-in commands work too
mquire query /path/to/memory.raw ".tables"
mquire query /path/to/memory.raw ".schema tasks"
Ejecute comandos personalizados para análisis especializados:```bash
mquire command /path/to/memory.raw
mquire command /path/to/memory.raw ".system_version"
mquire command /path/to/memory.raw ".task_tree"
mquire command /path/to/memory.raw ".task_tree --show-threads"
mquire command /path/to/memory.raw ".task_tree --help"
## Archivos SQL de Inicio Automático
mquire carga y ejecuta automáticamente los archivos SQL de `$HOME/.config/trailofbits/mquire/autostart/` al inicio. Los archivos están organizados por sistema operativo y arquitectura:```
autostart/
common/common/ # All platforms and architectures
common/{arch}/ # All platforms, specific architecture
{os}/common/ # Specific platform, all architectures
{os}/{arch}/ # Specific platform and architecture
Los archivos dentro de cada directorio se ordenan alfabéticamente. Los directorios se escanean en el orden mostrado anteriormente.
Características:
.sqlmquire_diagnostics pero no bloquean la ejecuciónmquire shell y mquire querymquire incluye vistas SQL reutilizables en el directorio sql/views/. Instálelas con just install-views. Consulte el README de vistas para ver la lista completa, la convención de numeración y la estructura de directorios.
Vistas de Linux (sql/views/linux/common/):
000_processes.sql - Lista de procesos deduplicada de todas las fuentes de descubrimiento, filtrada a líderes de proceso en espacio de usuario. Consultar con SELECT * FROM processes.100_process_network_connections.sql - Mapea las conexiones de red a los procesos propietarios mediante la unión a través de descriptores de archivo. Consultar con SELECT * FROM process_network_connections WHERE comm = 'sshd'.120_process_capabilities.sql - Resumen rápido de capacidades, una fila por proceso con cada conjunto como una lista compacta (ALL, '' o nombres de cap; NULL si es ilegible). Consultar con SELECT * FROM process_capabilities.Las consultas de mquire requieren reconstruir estructuras de datos del kernel a partir de la memoria virtual desreferenciando punteros utilizando información de tipo incrustada y símbolos de depuración. Este procesamiento puede ser costoso, por lo que usar técnicas de optimización de consultas mejora el rendimiento drásticamente.
AS MATERIALIZEDUse la sugerencia AS MATERIALIZED para almacenar en caché los resultados de las tablas cuando se usan en JOIN o se accede a ellas varias veces.
Cuándo materializar:
tasks requiere recorrer listas enlazadas de estructuras de proceso, desreferenciar múltiples punteros por proceso)Ejemplo:```sql -- Find network connections for a specific process using materialization WITH target_tasks AS MATERIALIZED ( SELECT * FROM tasks WHERE comm = 'sshd' AND type = 'thread_group_leader' ),
network_connections_mat AS MATERIALIZED ( SELECT * FROM network_connections )
SELECT t.tgid, t.comm, nc.local_address, nc.local_port, nc.remote_address, nc.remote_port, nc.state, nc.protocol FROM target_tasks t JOIN task_open_files tof ON tof.task = t.virtual_address JOIN network_connections_mat nc ON nc.inode = tof.inode;
**Nota:** Las tablas `task_open_files` y `memory_mappings` usan la columna `task` como entrada del generador. Cuando se unen con la tabla `tasks`, SQLite pasa automáticamente la restricción mediante uniones de bucle anidado, haciendo que las uniones directas sean eficientes.
**Impacto en el rendimiento:** La materialización puede proporcionar una aceleración significativa para consultas con JOINs (típicamente 2-5 veces más rápido)
**Resultados de benchmark de ejemplo:**
*Prueba realizada en una instantánea de Ubuntu 24.04 (kernel 6.8.0-63), 351 procesos, 50 conexiones, 2142 archivos abiertos. El rendimiento variará según el tamaño de la instantánea, la versión del kernel y el hardware.*
| Método | Tiempo real | Tiempo de usuario | Aceleración |
|--------|-------------|-------------------|-------------|
| SIN materialización | 12.067s | 16.373s | línea base |
| CON materialización | 3.171s | 8.786s | **3.8x más rápido** |
### Optimización del orden de JOIN
**Comience con la tabla más pequeña y únala hacia tablas más grandes** para minimizar las filas procesadas al principio de la canalización de consultas.
**Tamaños típicos de tablas:**
- `network_connections`: la más pequeña - solo procesos con actividad de red
- `tasks`: mediana - todos los procesos
- `task_open_files`: la más grande - todos los descriptores de archivos abiertos
**Orden óptimo:**
Comience con la tabla de tareas filtrada y únala hacia tablas más grandes:```sql
FROM target_tasks t -- filtered tasks
JOIN task_open_files tof ON tof.task = t.virtual_address -- open files
JOIN network_connections_mat nc ON nc.inode = tof.inode -- matching connections
Usa EXPLAIN QUERY PLAN para ver cómo SQLite ejecuta tu consulta:```sql
EXPLAIN QUERY PLAN
SELECT ...
FROM target_tasks t
JOIN task_open_files tof ON tof.task = t.virtual_address
JOIN network_connections_mat nc ON nc.inode = tof.inode;
Busca:
- **BLOOM FILTER**: Optimización de SQLite para JOINs grandes
- **AUTOMATIC COVERING INDEX**: Índices temporales creados para búsquedas
- **SCAN**: Escaneo completo de tabla (esperado para la tabla conductora)
- **SEARCH**: Búsqueda basada en índice (eficiente)
### Mejores prácticas
1. **Materializa siempre las tablas costosas** utilizadas en JOINs
2. **Empieza con la tabla más pequeña** como tu tabla conductora
3. **Usa SQL multilínea** en scripts para mejorar la legibilidad
4. **Verifica los planes de consulta** con `EXPLAIN QUERY PLAN` para consultas complejas
5. **Evita `SELECT *`** en producción - especifica solo las columnas necesarias
### Extracción de archivos
Extrae archivos de la memoria al disco:```bash
mquire command /path/to/memory.raw ".dump /output/directory"
Todas las consultas utilizan sintaxis SQL estándar.
$ mquire shell ubuntu2404_6.14.0-37-generic.lime mquire> SELECT * FROM os_version; arch:"x86_64" kernel_version:"6.14.0-37-generic" system_version:"#37~24.04.1-Ubuntu SMP PREEMPT_DYNAMIC Thu Nov 20 10:25:38 UTC 2"
#### Información del sistema```bash
$ mquire shell ubuntu2404_6.14.0-37-generic.lime
mquire> SELECT * FROM system_info;
domain:"(none)" hostname:"ubuntu2404"
$ mquire shell ubuntu2404_6.14.0-37-generic.lime mquire> SELECT name, state, src_version, parameters FROM kernel_modules LIMIT 5; name:"snd_seq_dummy" state:"live" src_version:"7A40E0FD47A0746D1C9CD85" parameters:"ump (perm: 0o444), duplex (perm: 0o444), ports (perm: 0o444)" name:"snd_hrtimer" state:"live" src_version:"81EE6D58896E2C2E63E252D" parameters:"" name:"qrtr" state:"live" src_version:"473C5AB47E04ECEA0106681" parameters:"" name:"virtio_rng" state:"live" src_version:"0852940240D554836D22CB2" parameters:"" name:"intel_rapl_msr" state:"live" src_version:"34853C4F5EB8FCAD28ACFB3" parameters:""
#### Ejecutar tareas```bash
$ mquire shell ubuntu2404_6.14.0-37-generic.lime
mquire> SELECT comm, binary_path, command_line FROM tasks WHERE command_line NOT NULL AND comm LIKE "%systemd%";
comm:"systemd" binary_path:"/usr/lib/systemd/systemd" command_line:"/sbin/init splash"
comm:"systemd-oomd" binary_path:"/usr/lib/systemd/systemd-oomd" command_line:"/usr/lib/systemd/systemd-oomd"
comm:"systemd-resolve" binary_path:"/usr/lib/systemd/systemd-resolved" command_line:"/usr/lib/systemd/systemd-resolved"
comm:"systemd-udevd" binary_path:"/usr/bin/udevadm" command_line:"/usr/lib/systemd/systemd-udevd"
comm:"systemd" binary_path:"/usr/lib/systemd/systemd" command_line:"/usr/lib/systemd/systemd --user"
comm:"systemd-logind" binary_path:"/usr/lib/systemd/systemd-logind" command_line:"/usr/lib/systemd/systemd-logind"
comm:"systemd-journal" binary_path:"/usr/lib/systemd/systemd-journald" command_line:"/usr/lib/systemd/systemd-journald"
comm:"systemd-timesyn" binary_path:"/usr/lib/systemd/systemd-timesyncd" command_line:"/usr/lib/systemd/systemd-timesyncd"
Encuentra conexiones de red para un proceso específico uniendo tasks, task_open_files y network_connections.```bash $ mquire shell ubuntu2404_6.14.0-37-generic.lime mquire> SELECT t.tgid, t.comm, nc.protocol, nc.local_address, nc.local_port, nc.remote_address, nc.remote_port, nc.state FROM tasks t JOIN task_open_files tof ON tof.task = t.virtual_address JOIN network_connections nc ON nc.inode = tof.inode WHERE t.comm = 'sshd'; tgid:"1134" comm:"sshd" protocol:"tcp" local_address:"0.0.0.0" local_port:"22" remote_address:"" remote_port:"" state:"listen" tgid:"1134" comm:"sshd" protocol:"tcp" local_address:"::" local_port:"22" remote_address:"" remote_port:"" state:"listen"
#### Archivos abiertos de tareas
Lista los archivos abiertos para procesos específicos uniendo `tasks` con `task_open_files`:```bash
$ mquire shell ubuntu2404_6.14.0-37-generic.lime
mquire> SELECT t.comm, tof.path
FROM tasks t
JOIN task_open_files tof ON tof.task = t.virtual_address
WHERE t.comm LIKE '%systemd%'
LIMIT 10;
comm:"systemd" path:"/null"
comm:"systemd" path:"/null"
comm:"systemd" path:"/null"
comm:"systemd" path:"/kmsg"
comm:"systemd" path:"[eventpoll]"
comm:"systemd" path:"[signalfd]"
comm:"systemd" path:"inotify"
comm:"systemd" path:"/"
comm:"systemd" path:"[timerfd]"
comm:"systemd" path:"/usr/lib/systemd/systemd-executor"
$ mquire query --format=json ubuntu2404_6.14.0-37-generic.lime "SELECT * FROM os_version" [ { "arch": "x86_64", "kernel_version": "6.14.0-37-generic", "system_version": "#37~24.04.1-Ubuntu SMP PREEMPT_DYNAMIC Thu Nov 20 10:25:38 UTC 2" } ]
##### Salida de tabla```bash
$ mquire query --format=table ubuntu2404_6.14.0-37-generic.lime "SELECT * FROM os_version"
arch:"x86_64" kernel_version:"6.14.0-37-generic" system_version:"#37~24.04.1-Ubuntu SMP PREEMPT_DYNAMIC Thu Nov 20 10:25:38 UTC 2"
$ mquire command ubuntu2404_6.14.0-37-generic.lime Available commands: .carve Carve a region of virtual memory to disk .dump Dump all open files from tasks to disk .system_version Display the operating system version .task_tree Display a hierarchical task tree
##### Mostrar versión del sistema```bash
$ mquire command ubuntu2404_6.14.0-37-generic.lime ".system_version"
System Version: #37~24.04.1-Ubuntu SMP PREEMPT_DYNAMIC Thu Nov 20 10:25:38 UTC 2
Kernel Version: 6.14.0-37-generic
Architecture: x86_64
$ mquire command ubuntu2404_6.8.0-63-generic.lime .task_tree | head -n 10 Parent: task_struct::parent Threads: Disabled Page Table: paddr(0x0000000001a60000)
└─ [0] (ffffffff90c0fcc0) swapper/0 ╎ ↳ [0] (ffff982a00e33518) \xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbd,)\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbd\x0e ├─ [1] (ffff982a0084a8c0) systemd │ ├─ [430] (ffff982a0d27a8c0) systemd-journal │ ├─ [495] (ffff982a08a88000) systemd-udevd │ ├─ [786] (ffff982a07a60000) systemd-oomd
**Nota:** Cuando existen múltiples entradas de `task_struct` con el mismo TID (ID de hilo, que puede ocurrir debido a corrupción de memoria o sincronización de instantáneas), las entradas duplicadas se muestran con el símbolo de continuación `╎ ↳` sangrado debajo de la entrada principal. El formato es `[TGID] (virtual_address) name` cuando los hilos están ocultos, o `[TGID TID]` cuando se muestran los hilos (donde TGID es el ID del grupo de hilos, comúnmente conocido como PID).
#### Extraer archivos de la memoria```bash
$ mquire command ubuntu2404_6.14.0-37-generic.lime ".dump ./extracted_files"
Legend: SK = skipped, OK = all good, ER = errored
Summary:
Total files processed: 1234
Successfully dumped: 1156
Skipped: 45
Errors: 33
File Status:
OK /usr/lib/systemd/systemd (TGID 1)
OK /etc/passwd (TGID 1)
SK /dev/null (TGID 1)
...
Utilice la opción --debug para habilitar mensajes de depuración detallados durante la inicialización y el análisis:```bash
mquire --debug command /path/to/memory.raw ".system_version"
- **`shell` y `query` modes**: Los mensajes de depuración se almacenan en la tabla SQL `mquire_diagnostics`. Consúltelos con `SELECT * FROM mquire_diagnostics;`
- **`command` mode**: Los mensajes de depuración se imprimen directamente en stderr.
Para problemas de inicialización que impidan que mquire cargue exitosamente la instantánea, se recomienda usar el modo `command` con un comando simple como `.system_version`, ya que imprime la salida de depuración en stderr inmediatamente sin necesidad de consultar la tabla `mquire_diagnostics`.
## Configuración
mquire se puede configurar a través de un archivo TOML en `$HOME/.config/trailofbits/mquire/config.toml`. Si el archivo no existe, se usan los valores predeterminados.
### Opciones disponibles y valores predeterminados```toml
[database]
# Maximum number of entries retained in the mquire_diagnostics table.
# During initialization, all entries are kept regardless of this limit.
# Once initialization completes, new log entries trigger eviction of the
# oldest entries when the total exceeds this value.
mquire_diagnostics_max_entries = 1000
Este proyecto utiliza just como ejecutor de comandos. Ejecute just para ver los comandos disponibles:
Estas pruebas verifican que mquire produzca la salida correcta para consultas contra instantáneas de memoria.
just integration-test - Ejecutar pruebas y comparar la salida con los archivos JSON esperadosjust integration-update - Actualizar los archivos JSON esperados con la salida real (usar al cambiar esquemas de tablas)Después de ejecutar integration-update, revise el diff de git para asegurarse de que los cambios coinciden con sus expectativas antes de realizar el commit.
Agregar nuevas pruebas: Cree un archivo .sql y un archivo .json correspondiente en el directorio de instantáneas apropiado, luego ejecute just integration-update para llenar la salida esperada.
¡Las contribuciones son bienvenidas! Al contribuir, siga estas pautas:
init_taskEste proyecto está licenciado bajo la Licencia Apache 2.0. Consulte el archivo LICENSE para más detalles.
130_process_ptrace_flags.sqlPT_*''SELECT * FROM process_ptrace_flags200_tasklist_pidns_differences.sql - Detecta procesos visibles en una fuente de descubrimiento pero ausentes en otra, útil para la detección de rootkits. Consultar con SELECT * FROM tasklist_pidns_differences.230_unbacked_ftrace_ops.sql - Callbacks ftrace registrados que apuntan fuera del texto del kernel y de cada módulo listado en kernel_modules. Consultar con SELECT * FROM unbacked_ftrace_ops.| Comando | Descripción |
|---|
just check | Ejecutar todas las comprobaciones (cargo check, cargo clippy, cargo fmt, ruff, mypy) |
just test | Ejecutar pruebas unitarias |
just format | Formatear código (cargo fmt, ruff) |
just integration-test | Ejecutar pruebas de integración de consultas SQL |
just integration-update | Actualizar la salida esperada de las pruebas |
just package | Construir paquetes de lanzamiento |