# ET-AUDIT-BACKEND: Servicios y API REST ## Identificacion | Campo | Valor | |-------|-------| | **ID** | ET-AUDIT-BACKEND | | **Modulo** | MGN-007 Audit | | **Version** | 1.0 | | **Estado** | En Diseno | | **Framework** | NestJS | | **Autor** | Requirements-Analyst | | **Fecha** | 2025-12-05 | --- ## Estructura de Archivos ``` apps/backend/src/modules/audit/ ├── audit.module.ts ├── controllers/ │ ├── audit-logs.controller.ts │ ├── access-logs.controller.ts │ ├── security-events.controller.ts │ └── scheduled-reports.controller.ts ├── services/ │ ├── audit-logs.service.ts │ ├── access-logs.service.ts │ ├── security-events.service.ts │ ├── security-alerts.service.ts │ └── scheduled-reports.service.ts ├── entities/ │ ├── audit-log.entity.ts │ ├── access-log.entity.ts │ ├── security-event.entity.ts │ ├── security-alert.entity.ts │ ├── scheduled-report.entity.ts │ └── audit-alert.entity.ts ├── dto/ │ ├── query-audit-logs.dto.ts │ ├── query-access-logs.dto.ts │ ├── create-security-event.dto.ts │ ├── resolve-security-event.dto.ts │ ├── create-scheduled-report.dto.ts │ └── export-audit.dto.ts ├── interceptors/ │ ├── audit.interceptor.ts │ └── access-log.interceptor.ts ├── subscribers/ │ └── entity-audit.subscriber.ts └── jobs/ ├── partition-maintenance.job.ts └── scheduled-reports.job.ts ``` --- ## Entidades ### AuditLog Entity ```typescript @Entity('audit_logs', { schema: 'core_audit' }) export class AuditLog { @PrimaryGeneratedColumn('uuid') id: string; @Column({ name: 'tenant_id', type: 'uuid' }) tenantId: string; @Column({ name: 'user_id', type: 'uuid', nullable: true }) userId: string; @Column({ length: 20 }) action: AuditAction; @Column({ name: 'entity_type', length: 100 }) entityType: string; @Column({ name: 'entity_id', type: 'uuid' }) entityId: string; @Column({ name: 'old_values', type: 'jsonb', nullable: true }) oldValues: Record; @Column({ name: 'new_values', type: 'jsonb', nullable: true }) newValues: Record; @Column({ name: 'changed_fields', type: 'text', array: true, nullable: true }) changedFields: string[]; @Column({ name: 'ip_address', type: 'inet', nullable: true }) ipAddress: string; @Column({ name: 'user_agent', type: 'text', nullable: true }) userAgent: string; @Column({ name: 'request_id', type: 'uuid', nullable: true }) requestId: string; @CreateDateColumn({ name: 'created_at' }) createdAt: Date; } export type AuditAction = 'create' | 'update' | 'delete' | 'restore'; ``` ### SecurityEvent Entity ```typescript @Entity('security_events', { schema: 'core_audit' }) export class SecurityEvent { @PrimaryGeneratedColumn('uuid') id: string; @Column({ name: 'tenant_id', type: 'uuid', nullable: true }) tenantId: string; @Column({ name: 'user_id', type: 'uuid', nullable: true }) userId: string; @Column({ name: 'event_type', length: 50 }) eventType: SecurityEventType; @Column({ length: 20 }) severity: SecuritySeverity; @Column({ length: 255 }) title: string; @Column({ type: 'text', nullable: true }) description: string; @Column({ type: 'jsonb', default: {} }) metadata: Record; @Column({ name: 'ip_address', type: 'inet', nullable: true }) ipAddress: string; @Column({ name: 'country_code', length: 2, nullable: true }) countryCode: string; @Column({ name: 'is_resolved', default: false }) isResolved: boolean; @Column({ name: 'resolved_by', type: 'uuid', nullable: true }) resolvedBy: string; @Column({ name: 'resolved_at', type: 'timestamptz', nullable: true }) resolvedAt: Date; @Column({ name: 'resolution_notes', type: 'text', nullable: true }) resolutionNotes: string; @CreateDateColumn({ name: 'created_at' }) createdAt: Date; @OneToMany(() => SecurityAlert, a => a.securityEvent) alerts: SecurityAlert[]; } export type SecuritySeverity = 'low' | 'medium' | 'high' | 'critical'; export type SecurityEventType = | 'login_failed' | 'login_blocked' | 'brute_force_detected' | 'password_changed' | 'password_reset_requested' | 'session_hijack_attempt' | 'concurrent_session' | 'session_from_new_location' | 'session_from_new_device' | 'permission_escalation' | 'role_changed' | 'admin_created' | 'mass_delete_attempt' | 'data_export' | 'sensitive_data_access' | 'api_key_created' | 'webhook_modified' | 'settings_changed'; ``` ### ScheduledReport Entity ```typescript @Entity('scheduled_reports', { schema: 'core_audit' }) export class ScheduledReport { @PrimaryGeneratedColumn('uuid') id: string; @Column({ name: 'tenant_id', type: 'uuid' }) tenantId: string; @Column({ length: 255 }) name: string; @Column({ name: 'report_type', length: 50 }) reportType: AuditReportType; @Column({ type: 'jsonb', default: {} }) filters: ReportFilters; @Column({ length: 10, default: 'pdf' }) format: ExportFormat; @Column({ length: 100 }) schedule: string; // cron expression @Column({ type: 'text', array: true }) recipients: string[]; @Column({ name: 'is_active', default: true }) isActive: boolean; @Column({ name: 'last_run_at', type: 'timestamptz', nullable: true }) lastRunAt: Date; @Column({ name: 'next_run_at', type: 'timestamptz', nullable: true }) nextRunAt: Date; @Column({ name: 'created_by', type: 'uuid' }) createdBy: string; @CreateDateColumn({ name: 'created_at' }) createdAt: Date; } export type AuditReportType = 'activity' | 'security' | 'compliance' | 'change' | 'access'; export type ExportFormat = 'csv' | 'xlsx' | 'pdf' | 'json'; ``` --- ## Servicios ### AuditLogsService ```typescript @Injectable() export class AuditLogsService { constructor( @InjectRepository(AuditLog) private readonly repo: Repository, @InjectDataSource() private readonly dataSource: DataSource, ) {} async findAll( tenantId: string, query: QueryAuditLogsDto ): Promise> { const qb = this.repo.createQueryBuilder('log') .where('log.tenant_id = :tenantId', { tenantId }); if (query.entityType) { qb.andWhere('log.entity_type = :entityType', { entityType: query.entityType }); } if (query.entityId) { qb.andWhere('log.entity_id = :entityId', { entityId: query.entityId }); } if (query.userId) { qb.andWhere('log.user_id = :userId', { userId: query.userId }); } if (query.action) { qb.andWhere('log.action = :action', { action: query.action }); } if (query.dateFrom) { qb.andWhere('log.created_at >= :dateFrom', { dateFrom: query.dateFrom }); } if (query.dateTo) { qb.andWhere('log.created_at <= :dateTo', { dateTo: query.dateTo }); } qb.orderBy('log.created_at', 'DESC'); return paginate(qb, query); } async getEntityHistory( tenantId: string, entityType: string, entityId: string ): Promise { return this.repo.find({ where: { tenantId, entityType, entityId }, order: { createdAt: 'DESC' }, }); } async log(data: CreateAuditLogDto): Promise { const log = this.repo.create(data); return this.repo.save(log); } async getStatsByAction( tenantId: string, dateFrom: Date, dateTo: Date ): Promise { const result = await this.repo .createQueryBuilder('log') .select('log.action', 'action') .addSelect('COUNT(*)', 'count') .where('log.tenant_id = :tenantId', { tenantId }) .andWhere('log.created_at BETWEEN :dateFrom AND :dateTo', { dateFrom, dateTo }) .groupBy('log.action') .getRawMany(); return result; } async getStatsByEntity( tenantId: string, dateFrom: Date, dateTo: Date ): Promise { const result = await this.repo .createQueryBuilder('log') .select('log.entity_type', 'entityType') .addSelect('COUNT(*)', 'count') .where('log.tenant_id = :tenantId', { tenantId }) .andWhere('log.created_at BETWEEN :dateFrom AND :dateTo', { dateFrom, dateTo }) .groupBy('log.entity_type') .orderBy('COUNT(*)', 'DESC') .limit(10) .getRawMany(); return result; } } ``` ### SecurityEventsService ```typescript @Injectable() export class SecurityEventsService { constructor( @InjectRepository(SecurityEvent) private readonly repo: Repository, private readonly alertsService: SecurityAlertsService, private readonly notificationsService: NotificationsService, ) {} async findAll( tenantId: string | null, query: QuerySecurityEventsDto ): Promise> { const qb = this.repo.createQueryBuilder('event'); if (tenantId) { qb.where('event.tenant_id = :tenantId', { tenantId }); } if (query.severity) { qb.andWhere('event.severity = :severity', { severity: query.severity }); } if (query.eventType) { qb.andWhere('event.event_type = :eventType', { eventType: query.eventType }); } if (query.isResolved !== undefined) { qb.andWhere('event.is_resolved = :isResolved', { isResolved: query.isResolved }); } if (query.dateFrom) { qb.andWhere('event.created_at >= :dateFrom', { dateFrom: query.dateFrom }); } qb.orderBy('event.created_at', 'DESC'); return paginate(qb, query); } async create(dto: CreateSecurityEventDto): Promise { const event = this.repo.create(dto); const saved = await this.repo.save(event); // Auto-alert for high/critical severity if (['high', 'critical'].includes(dto.severity)) { await this.triggerAlerts(saved); } return saved; } async resolve( id: string, userId: string, dto: ResolveSecurityEventDto ): Promise { const event = await this.repo.findOneOrFail({ where: { id } }); event.isResolved = true; event.resolvedBy = userId; event.resolvedAt = new Date(); event.resolutionNotes = dto.notes; return this.repo.save(event); } async getUnresolvedCount(tenantId?: string): Promise { const qb = this.repo.createQueryBuilder('event') .select('event.severity', 'severity') .addSelect('COUNT(*)', 'count') .where('event.is_resolved = false'); if (tenantId) { qb.andWhere('event.tenant_id = :tenantId', { tenantId }); } qb.groupBy('event.severity'); const results = await qb.getRawMany(); return { low: parseInt(results.find(r => r.severity === 'low')?.count || '0'), medium: parseInt(results.find(r => r.severity === 'medium')?.count || '0'), high: parseInt(results.find(r => r.severity === 'high')?.count || '0'), critical: parseInt(results.find(r => r.severity === 'critical')?.count || '0'), }; } private async triggerAlerts(event: SecurityEvent): Promise { // Get admin users to notify const admins = await this.getSecurityAdmins(event.tenantId); const alert = await this.alertsService.create({ securityEventId: event.id, sentTo: admins.map(a => a.id), channels: ['in_app', 'email'], }); // Send notifications for (const admin of admins) { await this.notificationsService.send({ userId: admin.id, type: 'security_alert', title: `Security Alert: ${event.title}`, body: event.description, data: { eventId: event.id, severity: event.severity }, channels: ['in_app', 'email'], }); } } } ``` ### AccessLogsService ```typescript @Injectable() export class AccessLogsService { constructor( @InjectRepository(AccessLog) private readonly repo: Repository, ) {} async findAll( tenantId: string | null, query: QueryAccessLogsDto ): Promise> { const qb = this.repo.createQueryBuilder('log'); if (tenantId) { qb.where('log.tenant_id = :tenantId', { tenantId }); } if (query.userId) { qb.andWhere('log.user_id = :userId', { userId: query.userId }); } if (query.path) { qb.andWhere('log.path LIKE :path', { path: `%${query.path}%` }); } if (query.method) { qb.andWhere('log.method = :method', { method: query.method }); } if (query.statusCode) { qb.andWhere('log.status_code = :statusCode', { statusCode: query.statusCode }); } if (query.minResponseTime) { qb.andWhere('log.response_time_ms >= :minResponseTime', { minResponseTime: query.minResponseTime }); } if (query.dateFrom) { qb.andWhere('log.created_at >= :dateFrom', { dateFrom: query.dateFrom }); } if (query.dateTo) { qb.andWhere('log.created_at <= :dateTo', { dateTo: query.dateTo }); } qb.orderBy('log.created_at', 'DESC'); return paginate(qb, query); } async log(data: CreateAccessLogDto): Promise { // Insert async - no wait for performance this.repo.insert(data).catch(err => { console.error('Failed to log access:', err); }); } async getAggregatedStats( tenantId: string, dateFrom: Date, dateTo: Date, groupBy: 'hour' | 'day' ): Promise { const dateFormat = groupBy === 'hour' ? "to_char(log.created_at, 'YYYY-MM-DD HH24:00')" : "to_char(log.created_at, 'YYYY-MM-DD')"; const result = await this.repo .createQueryBuilder('log') .select(dateFormat, 'period') .addSelect('COUNT(*)', 'totalRequests') .addSelect('AVG(log.response_time_ms)', 'avgResponseTime') .addSelect('COUNT(*) FILTER (WHERE log.status_code >= 400)', 'errorCount') .where('log.tenant_id = :tenantId', { tenantId }) .andWhere('log.created_at BETWEEN :dateFrom AND :dateTo', { dateFrom, dateTo }) .groupBy('period') .orderBy('period', 'ASC') .getRawMany(); return result; } async getTopEndpoints( tenantId: string, dateFrom: Date, limit: number = 10 ): Promise { return this.repo .createQueryBuilder('log') .select('log.method', 'method') .addSelect('log.path', 'path') .addSelect('COUNT(*)', 'count') .addSelect('AVG(log.response_time_ms)', 'avgResponseTime') .where('log.tenant_id = :tenantId', { tenantId }) .andWhere('log.created_at >= :dateFrom', { dateFrom }) .groupBy('log.method, log.path') .orderBy('COUNT(*)', 'DESC') .limit(limit) .getRawMany(); } } ``` ### ScheduledReportsService ```typescript @Injectable() export class ScheduledReportsService { constructor( @InjectRepository(ScheduledReport) private readonly repo: Repository, private readonly auditLogsService: AuditLogsService, private readonly accessLogsService: AccessLogsService, private readonly securityEventsService: SecurityEventsService, private readonly emailService: EmailService, private readonly exportService: ExportService, ) {} async findAll(tenantId: string): Promise { return this.repo.find({ where: { tenantId }, order: { createdAt: 'DESC' }, }); } async create( tenantId: string, userId: string, dto: CreateScheduledReportDto ): Promise { const report = this.repo.create({ ...dto, tenantId, createdBy: userId, nextRunAt: this.calculateNextRun(dto.schedule), }); return this.repo.save(report); } async update( id: string, dto: UpdateScheduledReportDto ): Promise { const report = await this.repo.findOneOrFail({ where: { id } }); Object.assign(report, dto); if (dto.schedule) { report.nextRunAt = this.calculateNextRun(dto.schedule); } return this.repo.save(report); } async delete(id: string): Promise { await this.repo.delete(id); } async runReport(id: string): Promise { const report = await this.repo.findOneOrFail({ where: { id } }); const data = await this.fetchReportData(report); return this.exportService.export(data, report.format, report.reportType); } @Cron('0 * * * *') // Every hour async processScheduledReports(): Promise { const dueReports = await this.repo.find({ where: { isActive: true, nextRunAt: LessThanOrEqual(new Date()), }, }); for (const report of dueReports) { try { const data = await this.fetchReportData(report); const file = await this.exportService.export(data, report.format, report.reportType); await this.emailService.sendWithAttachment({ to: report.recipients, subject: `Audit Report: ${report.name}`, body: `Attached is your scheduled ${report.reportType} report.`, attachment: { filename: `${report.name}_${format(new Date(), 'yyyy-MM-dd')}.${report.format}`, content: file, }, }); report.lastRunAt = new Date(); report.nextRunAt = this.calculateNextRun(report.schedule); await this.repo.save(report); } catch (error) { console.error(`Failed to run report ${report.id}:`, error); } } } private async fetchReportData(report: ScheduledReport): Promise { const { dateFrom, dateTo } = this.getDateRange(report.filters); switch (report.reportType) { case 'activity': case 'change': return this.auditLogsService.findAll(report.tenantId, { dateFrom, dateTo, ...report.filters, limit: 10000, }).then(r => r.items); case 'access': return this.accessLogsService.findAll(report.tenantId, { dateFrom, dateTo, ...report.filters, limit: 10000, }).then(r => r.items); case 'security': return this.securityEventsService.findAll(report.tenantId, { dateFrom, dateTo, ...report.filters, limit: 10000, }).then(r => r.items); default: return []; } } private calculateNextRun(cronExpression: string): Date { const interval = parseExpression(cronExpression); return interval.next().toDate(); } } ``` --- ## Interceptores ### AuditInterceptor ```typescript @Injectable() export class AuditInterceptor implements NestInterceptor { constructor( private readonly auditService: AuditLogsService, private readonly cls: ClsService, ) {} intercept(context: ExecutionContext, next: CallHandler): Observable { const request = context.switchToHttp().getRequest(); const handler = context.getHandler(); const auditConfig = Reflect.getMetadata('audit', handler); if (!auditConfig) { return next.handle(); } return next.handle().pipe( tap(async (response) => { try { await this.auditService.log({ tenantId: request.tenantId, userId: request.user?.id, action: auditConfig.action, entityType: auditConfig.entityType, entityId: response?.id || request.params?.id, oldValues: this.cls.get('oldValues'), newValues: response, ipAddress: request.ip, userAgent: request.headers['user-agent'], requestId: request.id, }); } catch (error) { console.error('Audit log failed:', error); } }), ); } } // Decorator export const Audit = (config: { action: AuditAction; entityType: string }) => SetMetadata('audit', config); // Usage @Post() @Audit({ action: 'create', entityType: 'contact' }) async create(@Body() dto: CreateContactDto) { ... } ``` ### AccessLogInterceptor ```typescript @Injectable() export class AccessLogInterceptor implements NestInterceptor { constructor(private readonly accessLogsService: AccessLogsService) {} intercept(context: ExecutionContext, next: CallHandler): Observable { const request = context.switchToHttp().getRequest(); const startTime = Date.now(); return next.handle().pipe( finalize(() => { const response = context.switchToHttp().getResponse(); this.accessLogsService.log({ tenantId: request.tenantId, userId: request.user?.id, sessionId: request.sessionId, requestId: request.id, method: request.method, path: request.route?.path || request.path, queryParams: request.query, statusCode: response.statusCode, responseTimeMs: Date.now() - startTime, ipAddress: request.ip, userAgent: request.headers['user-agent'], referer: request.headers['referer'], }); }), ); } } ``` --- ## TypeORM Subscriber ### EntityAuditSubscriber ```typescript @EventSubscriber() export class EntityAuditSubscriber implements EntitySubscriberInterface { private readonly auditableEntities = [ 'Contact', 'User', 'Role', 'Tenant', 'Setting', ]; constructor( dataSource: DataSource, private readonly auditService: AuditLogsService, private readonly cls: ClsService, ) { dataSource.subscribers.push(this); } listenTo() { return Object.class; // Listen to all entities } beforeUpdate(event: UpdateEvent): void { if (this.shouldAudit(event.metadata.name)) { // Store old values for comparison this.cls.set('oldValues', event.databaseEntity); } } afterInsert(event: InsertEvent): Promise { return this.logChange(event, 'create'); } afterUpdate(event: UpdateEvent): Promise { return this.logChange(event, 'update'); } afterRemove(event: RemoveEvent): Promise { return this.logChange(event, 'delete'); } afterSoftRemove(event: SoftRemoveEvent): Promise { return this.logChange(event, 'delete'); } afterRecover(event: RecoverEvent): Promise { return this.logChange(event, 'restore'); } private shouldAudit(entityName: string): boolean { return this.auditableEntities.includes(entityName); } private async logChange( event: any, action: AuditAction ): Promise { if (!this.shouldAudit(event.metadata.name)) { return; } const entity = event.entity || event.databaseEntity; if (!entity) return; try { await this.auditService.log({ tenantId: this.cls.get('tenantId'), userId: this.cls.get('userId'), action, entityType: event.metadata.tableName, entityId: entity.id, oldValues: action === 'update' ? this.cls.get('oldValues') : null, newValues: action === 'delete' ? null : entity, ipAddress: this.cls.get('ipAddress'), requestId: this.cls.get('requestId'), }); } catch (error) { console.error('Entity audit failed:', error); } } } ``` --- ## Controladores ### AuditLogsController ```typescript @ApiTags('Audit Logs') @Controller('audit/logs') @UseGuards(JwtAuthGuard, RbacGuard) export class AuditLogsController { constructor(private readonly service: AuditLogsService) {} @Get() @Permissions('audit.logs.read') async findAll( @TenantId() tenantId: string, @Query() query: QueryAuditLogsDto ) { return this.service.findAll(tenantId, query); } @Get('entity/:entityType/:entityId') @Permissions('audit.logs.read') async getEntityHistory( @TenantId() tenantId: string, @Param('entityType') entityType: string, @Param('entityId') entityId: string ) { return this.service.getEntityHistory(tenantId, entityType, entityId); } @Get('stats/actions') @Permissions('audit.logs.read') async getStatsByAction( @TenantId() tenantId: string, @Query('dateFrom') dateFrom: Date, @Query('dateTo') dateTo: Date ) { return this.service.getStatsByAction(tenantId, dateFrom, dateTo); } @Get('stats/entities') @Permissions('audit.logs.read') async getStatsByEntity( @TenantId() tenantId: string, @Query('dateFrom') dateFrom: Date, @Query('dateTo') dateTo: Date ) { return this.service.getStatsByEntity(tenantId, dateFrom, dateTo); } @Post('export') @Permissions('audit.logs.export') async export( @TenantId() tenantId: string, @Body() dto: ExportAuditDto ) { const data = await this.service.findAll(tenantId, dto); return this.service.export(data.items, dto.format); } } ``` ### SecurityEventsController ```typescript @ApiTags('Security Events') @Controller('audit/security') @UseGuards(JwtAuthGuard, RbacGuard) export class SecurityEventsController { constructor(private readonly service: SecurityEventsService) {} @Get() @Permissions('audit.security.read') async findAll( @TenantId() tenantId: string, @Query() query: QuerySecurityEventsDto ) { return this.service.findAll(tenantId, query); } @Get('counts') @Permissions('audit.security.read') async getUnresolvedCounts(@TenantId() tenantId: string) { return this.service.getUnresolvedCount(tenantId); } @Patch(':id/resolve') @Permissions('audit.security.manage') async resolve( @Param('id') id: string, @CurrentUser() user: User, @Body() dto: ResolveSecurityEventDto ) { return this.service.resolve(id, user.id, dto); } } // Admin controller for global events @ApiTags('Security Events Admin') @Controller('admin/audit/security') @UseGuards(JwtAuthGuard, RbacGuard) @Permissions('admin.security.manage') export class SecurityEventsAdminController { constructor(private readonly service: SecurityEventsService) {} @Get() async findAll(@Query() query: QuerySecurityEventsDto) { return this.service.findAll(null, query); } @Get('counts') async getGlobalCounts() { return this.service.getUnresolvedCount(); } } ``` ### ScheduledReportsController ```typescript @ApiTags('Audit Reports') @Controller('audit/reports') @UseGuards(JwtAuthGuard, RbacGuard) export class ScheduledReportsController { constructor(private readonly service: ScheduledReportsService) {} @Get() @Permissions('audit.reports.read') async findAll(@TenantId() tenantId: string) { return this.service.findAll(tenantId); } @Post() @Permissions('audit.reports.manage') async create( @TenantId() tenantId: string, @CurrentUser() user: User, @Body() dto: CreateScheduledReportDto ) { return this.service.create(tenantId, user.id, dto); } @Patch(':id') @Permissions('audit.reports.manage') async update( @Param('id') id: string, @Body() dto: UpdateScheduledReportDto ) { return this.service.update(id, dto); } @Delete(':id') @Permissions('audit.reports.manage') async delete(@Param('id') id: string) { return this.service.delete(id); } @Post(':id/run') @Permissions('audit.reports.manage') async runNow(@Param('id') id: string, @Res() res: Response) { const buffer = await this.service.runReport(id); res.setHeader('Content-Type', 'application/octet-stream'); res.send(buffer); } } ``` --- ## DTOs ### Query DTOs ```typescript export class QueryAuditLogsDto extends PaginationDto { @IsOptional() @IsString() entityType?: string; @IsOptional() @IsUUID() entityId?: string; @IsOptional() @IsUUID() userId?: string; @IsOptional() @IsIn(['create', 'update', 'delete', 'restore']) action?: AuditAction; @IsOptional() @IsDateString() dateFrom?: string; @IsOptional() @IsDateString() dateTo?: string; } export class QuerySecurityEventsDto extends PaginationDto { @IsOptional() @IsIn(['low', 'medium', 'high', 'critical']) severity?: SecuritySeverity; @IsOptional() @IsString() eventType?: string; @IsOptional() @IsBoolean() @Transform(({ value }) => value === 'true') isResolved?: boolean; @IsOptional() @IsDateString() dateFrom?: string; } ``` ### Create/Update DTOs ```typescript export class CreateScheduledReportDto { @IsString() @Length(1, 255) name: string; @IsIn(['activity', 'security', 'compliance', 'change', 'access']) reportType: AuditReportType; @IsObject() filters: Record; @IsIn(['csv', 'xlsx', 'pdf', 'json']) @IsOptional() format?: ExportFormat = 'pdf'; @IsString() schedule: string; // cron expression @IsArray() @IsEmail({}, { each: true }) recipients: string[]; } export class ResolveSecurityEventDto { @IsString() @IsOptional() notes?: string; } ``` --- ## API Endpoints Summary | Method | Path | Permission | Description | |--------|------|------------|-------------| | GET | /audit/logs | audit.logs.read | List audit logs | | GET | /audit/logs/entity/:type/:id | audit.logs.read | Get entity history | | GET | /audit/logs/stats/actions | audit.logs.read | Stats by action | | GET | /audit/logs/stats/entities | audit.logs.read | Stats by entity type | | POST | /audit/logs/export | audit.logs.export | Export logs | | GET | /audit/access | audit.access.read | List access logs | | GET | /audit/access/stats | audit.access.read | Access statistics | | GET | /audit/access/endpoints | audit.access.read | Top endpoints | | GET | /audit/security | audit.security.read | List security events | | GET | /audit/security/counts | audit.security.read | Unresolved counts | | PATCH | /audit/security/:id/resolve | audit.security.manage | Resolve event | | GET | /audit/reports | audit.reports.read | List scheduled reports | | POST | /audit/reports | audit.reports.manage | Create report | | PATCH | /audit/reports/:id | audit.reports.manage | Update report | | DELETE | /audit/reports/:id | audit.reports.manage | Delete report | | POST | /audit/reports/:id/run | audit.reports.manage | Run report now | --- ## Historial | Version | Fecha | Autor | Cambios | |---------|-------|-------|---------| | 1.0 | 2025-12-05 | Requirements-Analyst | Creacion inicial |