imagy-audit — Modelo de Datos
Base de Datos
- Nombre:
imagy_audit - Usuario:
imagy_audit_app(NOBYPASSRLS) - RLS: Habilitado en todas las tablas (tenant_id)
- Inmutabilidad: UPDATE y DELETE revocados en
audit_events
Diagrama ER
Tablas
audit_events (particionada por mes)
Tabla principal de eventos de auditoria. Particionada por rango de created_at con granularidad mensual. Cada particion contiene los eventos de un mes calendario.
| Columna | Tipo | Descripcion |
|---|---|---|
id | UUID PK | Identificador unico del evento |
tenant_id | UUID | Tenant propietario (RLS) |
class_uid | INT | Clase OCSF (ej: 3001 = Account Change) |
class_name | VARCHAR(100) | Nombre de la clase OCSF |
category_uid | INT | Categoria OCSF (ej: 3 = IAM) |
category_name | VARCHAR(100) | Nombre de la categoria OCSF |
activity_id | INT | Actividad OCSF (1=Create, 2=Update, 3=Delete, etc.) |
activity_name | VARCHAR(100) | Nombre de la actividad |
severity_id | INT | Severidad (1=Informational, 2=Low, 3=Medium, 4=High, 5=Critical) |
time | BIGINT | Timestamp del evento original (epoch milliseconds) |
message | TEXT | Descripcion legible del evento |
actor_id | UUID FK | Referencia al actor que ejecuto la accion |
resource_id | UUID FK | Referencia al recurso afectado (nullable) |
src_endpoint | JSONB | Endpoint de origen (ip, name, service) |
metadata | JSONB | Metadata OCSF (version, product, tenant_uid, profiles) |
unmapped | JSONB | Campos de extension — datos de dominio no mapeados a OCSF |
created_at | TIMESTAMPTZ | Fecha de insercion (clave de particionamiento) |
Inmutabilidad: Esta tabla no permite UPDATE ni DELETE a nivel de permisos de base de datos.
audit_actors
Catalogo de actores que han generado eventos. Se actualiza con last_seen_at cada vez que el actor aparece en un nuevo evento.
| Columna | Tipo | Descripcion |
|---|---|---|
id | UUID PK | Identificador interno |
tenant_id | UUID | Tenant (RLS) |
user_uid | VARCHAR(255) | UID del usuario o servicio en la plataforma |
user_name | VARCHAR(255) | Nombre del actor |
user_type | VARCHAR(50) | User, System, Service, Admin |
session_uid | VARCHAR(255) | Ultima sesion conocida (nullable) |
service_name | VARCHAR(100) | Nombre del servicio si es actor de tipo System/Service |
first_seen_at | TIMESTAMPTZ | Primera aparicion |
last_seen_at | TIMESTAMPTZ | Ultima aparicion |
Constraint: UNIQUE(tenant_id, user_uid)
audit_resources
Catalogo de recursos que han sido referenciados en eventos de auditoria.
| Columna | Tipo | Descripcion |
|---|---|---|
id | UUID PK | Identificador interno |
tenant_id | UUID | Tenant (RLS) |
resource_uid | VARCHAR(255) | UID del recurso en su dominio de origen |
resource_type | VARCHAR(100) | Tipo de recurso (credit, application, user, document, flow) |
resource_name | VARCHAR(255) | Nombre legible del recurso (nullable) |
domain | VARCHAR(50) | Dominio de origen (imaglend, imagflow, imagsign, imagid, identity) |
first_seen_at | TIMESTAMPTZ | Primera referencia |
last_seen_at | TIMESTAMPTZ | Ultima referencia |
Constraint: UNIQUE(tenant_id, resource_uid, resource_type)
audit_export_jobs
Jobs de exportacion solicitados por los tenants.
| Columna | Tipo | Descripcion |
|---|---|---|
id | UUID PK | Identificador del job |
tenant_id | UUID | Tenant (RLS) |
status | VARCHAR(20) | pending, processing, completed, failed |
format | VARCHAR(20) | ocsf_json, csv |
filters | JSONB | Filtros aplicados (periodo, categoria, actor, severidad) |
file_url | TEXT | URL de descarga (S3 presigned, nullable hasta completar) |
total_events | BIGINT | Cantidad de eventos exportados |
requested_at | TIMESTAMPTZ | Fecha de solicitud |
started_at | TIMESTAMPTZ | Inicio del procesamiento (nullable) |
completed_at | TIMESTAMPTZ | Finalizacion (nullable) |
requested_by | UUID | Usuario que solicito la exportacion |
Campos de Extension (unmapped)
El campo unmapped de tipo JSONB almacena datos especificos de dominio que no mapean directamente a campos OCSF estandar. Esto permite extender el schema sin romper la compatibilidad.
Ejemplos por dominio:
// ImagLend — credito aprobado
{
"credit_id": "credit-uuid-001",
"product_id": "microcredito-express",
"amount": { "value": 500000, "currency": "COP" },
"decision": "approved",
"pipeline_step": "disbursement"
}
// ImagSign — documento firmado
{
"signature_id": "sig-uuid-001",
"document_type": "credit_contract",
"signer_document": "****7890",
"signature_method": "electronic"
}
// ImagID — sujeto verificado
{
"subject_id": "subj-uuid-001",
"verification_type": "liveness",
"provider": "onfido",
"score": 0.97
}Estrategia de Particionamiento
La tabla audit_events esta particionada por rango mensual sobre created_at. Las particiones se crean automaticamente con anticipacion (3 meses adelante) mediante un job programado.
-- Tabla padre (particionada)
CREATE TABLE audit_events (
id UUID NOT NULL DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
class_uid INT NOT NULL,
class_name VARCHAR(100) NOT NULL,
category_uid INT NOT NULL,
category_name VARCHAR(100) NOT NULL,
activity_id INT NOT NULL,
activity_name VARCHAR(100) NOT NULL,
severity_id INT NOT NULL DEFAULT 1,
time BIGINT NOT NULL,
message TEXT,
actor_id UUID REFERENCES audit_actors(id),
resource_id UUID REFERENCES audit_resources(id),
src_endpoint JSONB,
metadata JSONB NOT NULL,
unmapped JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (id, created_at)
) PARTITION BY RANGE (created_at);
-- Particiones mensuales (auto-creadas)
CREATE TABLE audit_events_2026_01 PARTITION OF audit_events
FOR VALUES FROM ('2026-01-01') TO ('2026-02-01');
CREATE TABLE audit_events_2026_02 PARTITION OF audit_events
FOR VALUES FROM ('2026-02-01') TO ('2026-03-01');
-- ... se crean automaticamente via pg_cronCiclo de Vida de Particiones
- Hot (0-90 dias): Particiones en tablespace SSD, indices completos, consulta directa.
- Warm (91-365 dias): Particiones en tablespace HDD, indices reducidos, consulta mas lenta.
- Cold (> 365 dias): Datos exportados a S3 Glacier, particion eliminada de PostgreSQL. Acceso solo via exportacion bajo demanda.
Inmutabilidad
La inmutabilidad se garantiza a nivel de base de datos revocando permisos de modificacion:
-- Revocar UPDATE y DELETE en audit_events para el usuario de aplicacion
REVOKE UPDATE, DELETE ON audit_events FROM imagy_audit_app;
-- Trigger de seguridad adicional (defense in depth)
CREATE OR REPLACE FUNCTION prevent_audit_modification()
RETURNS TRIGGER AS $$
BEGIN
RAISE EXCEPTION 'audit_events is immutable: % operations are not allowed', TG_OP;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_prevent_update
BEFORE UPDATE ON audit_events
FOR EACH ROW EXECUTE FUNCTION prevent_audit_modification();
CREATE TRIGGER trg_prevent_delete
BEFORE DELETE ON audit_events
FOR EACH ROW EXECUTE FUNCTION prevent_audit_modification();RLS Policies
ALTER TABLE audit_events ENABLE ROW LEVEL SECURITY;
ALTER TABLE audit_actors ENABLE ROW LEVEL SECURITY;
ALTER TABLE audit_resources ENABLE ROW LEVEL SECURITY;
ALTER TABLE audit_export_jobs ENABLE ROW LEVEL SECURITY;
-- Cada tenant solo ve sus propios eventos
CREATE POLICY tenant_isolation ON audit_events
FOR ALL USING (tenant_id = get_current_tenant_id());
CREATE POLICY tenant_isolation ON audit_actors
FOR ALL USING (tenant_id = get_current_tenant_id());
CREATE POLICY tenant_isolation ON audit_resources
FOR ALL USING (tenant_id = get_current_tenant_id());
CREATE POLICY tenant_isolation ON audit_export_jobs
FOR ALL USING (tenant_id = get_current_tenant_id());
-- Control plane para platform admin (acceso cross-tenant)
CREATE POLICY control_plane ON audit_events
FOR SELECT USING (
current_setting('app.current_tenant_id', true) IS NULL
OR current_setting('app.current_tenant_id', true) = ''
);Indices
-- Consulta por tenant y rango de tiempo (query mas comun)
CREATE INDEX idx_audit_events_tenant_time
ON audit_events(tenant_id, created_at DESC);
-- Consulta por actor
CREATE INDEX idx_audit_events_actor
ON audit_events(tenant_id, actor_id, created_at DESC);
-- Consulta por recurso
CREATE INDEX idx_audit_events_resource
ON audit_events(tenant_id, resource_id, created_at DESC);
-- Consulta por severidad (alertas, findings)
CREATE INDEX idx_audit_events_severity
ON audit_events(tenant_id, severity_id, created_at DESC)
WHERE severity_id >= 3;
-- Consulta por categoria y clase OCSF
CREATE INDEX idx_audit_events_category
ON audit_events(tenant_id, category_uid, class_uid, created_at DESC);
-- Consulta por time (epoch) para rangos exactos
CREATE INDEX idx_audit_events_time
ON audit_events(tenant_id, time DESC);
-- Actores
CREATE UNIQUE INDEX idx_audit_actors_uid
ON audit_actors(tenant_id, user_uid);
-- Recursos
CREATE UNIQUE INDEX idx_audit_resources_uid
ON audit_resources(tenant_id, resource_uid, resource_type);
-- Export jobs por estado
CREATE INDEX idx_audit_exports_status
ON audit_export_jobs(tenant_id, status, requested_at DESC);Tablas de Soporte
idempotency_keys
Previene el procesamiento duplicado de eventos consumidos desde RabbitMQ.
| Columna | Tipo | Descripcion |
|---|---|---|
key | VARCHAR(255) PK | Clave de idempotencia (event_id del mensaje) |
created_at | TIMESTAMPTZ | Fecha de registro |
expires_at | TIMESTAMPTZ | Expiracion (TTL de 7 dias) |
processed_events
Registro de eventos de dominio ya procesados para garantizar exactly-once semantics.
| Columna | Tipo | Descripcion |
|---|---|---|
event_id | UUID PK | ID del evento de dominio |
source | VARCHAR(100) | Servicio de origen |
routing_key | VARCHAR(255) | Routing key del mensaje |
processed_at | TIMESTAMPTZ | Fecha de procesamiento |