Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ model User {
// Primary wallet address — canonical identifier for the user
walletAddress String @unique

role String @default("USER")

reputation Int @default(0)

// Sybil Resistance
Expand Down
23 changes: 21 additions & 2 deletions src/audit/audit.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,34 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { ScheduleModule } from '@nestjs/schedule';
import { AuditLog } from './entities/audit-log.entity';
import { AuditTrailService } from './services/audit-trail.service';
import { AuditSearchService } from './services/audit-search.service';
import { AuditComplianceService } from './services/audit-compliance.service';
import { AuditMetricsService } from './services/audit-metrics.service';
import { AuditRetentionService } from './services/audit-retention.service';
import { AuditController } from './controllers/audit-log.controller';
import { AuditLoggingInterceptor } from './interceptors/audit-logging.interceptor';
import { RolesGuard } from '../auth/guards/roles.guard';

@Global()
@Module({
imports: [TypeOrmModule.forFeature([AuditLog]), ScheduleModule],
providers: [AuditTrailService, AuditLoggingInterceptor, AuditRetentionService],
providers: [
AuditTrailService,
AuditSearchService,
AuditComplianceService,
AuditMetricsService,
AuditRetentionService,
AuditLoggingInterceptor,
RolesGuard,
],
controllers: [AuditController],
exports: [AuditTrailService, AuditLoggingInterceptor],
exports: [
AuditTrailService,
AuditSearchService,
AuditComplianceService,
AuditMetricsService,
AuditRetentionService,
AuditLoggingInterceptor,
],
})
export class AuditModule {}
215 changes: 159 additions & 56 deletions src/audit/controllers/audit-log.controller.ts
Original file line number Diff line number Diff line change
@@ -1,94 +1,78 @@
import { Controller, Get, Query, Param, UseInterceptors } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiQuery } from '@nestjs/swagger';
import {
Controller, Get, Post, Patch, Query, Param, Body,
UseGuards, Req,
} from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiQuery, ApiBody } from '@nestjs/swagger';
import { AuditTrailService } from '../services/audit-trail.service';
import { AuditSearchService } from '../services/audit-search.service';
import { AuditComplianceService } from '../services/audit-compliance.service';
import { AuditMetricsService } from '../services/audit-metrics.service';
import { AuditRetentionService } from '../services/audit-retention.service';
import { AuditLog, AuditActionType, AuditEntityType } from '../entities/audit-log.entity';
import { SearchAuditDto } from '../dto/search-audit.dto';
import { GenerateReportDto, ReportType, ReportFormat } from '../dto/compliance-report.dto';
import { JwtAuthGuard } from '../../auth/jwt-auth.guard';
import { RolesGuard } from '../../auth/guards/roles.guard';
import { Roles } from '../../auth/decorators/roles.decorator';
import { UserRole } from '../../entities/user.entity';
import { CurrentUser } from '../../common/decorators/current-user.decorator';

@ApiTags('audit')
@Controller('audit')
export class AuditController {
constructor(private readonly auditTrailService: AuditTrailService) {}
constructor(
private readonly auditTrailService: AuditTrailService,
private readonly auditSearchService: AuditSearchService,
private readonly auditComplianceService: AuditComplianceService,
private readonly auditMetricsService: AuditMetricsService,
private readonly auditRetentionService: AuditRetentionService,
) {}

/**
* Get all audit logs with optional filters
* GET /audit?entityType=CLAIM&actionType=CLAIM_CREATED&userId=xxx&limit=50&offset=0
*/
@Get()
async getAuditLogs(
@Query('entityType') entityType?: AuditEntityType,
@Query('actionType') actionType?: AuditActionType,
@Query('userId') userId?: string,
@Query('limit') limit?: string,
@Query('offset') offset?: string,
): Promise<{ logs: AuditLog[]; total: number }> {
const parsedLimit = limit ? Math.min(parseInt(limit, 10), 500) : 100;
const parsedOffset = offset ? Math.max(parseInt(offset, 10), 0) : 0;

return this.auditTrailService.getAuditLogs(
entityType,
actionType,
userId,
parsedLimit,
parsedOffset,
);
@ApiOperation({ summary: 'Search audit logs with advanced filters' })
@ApiResponse({ status: 200, description: 'Returns filtered audit logs' })
async search(@Query() dto: SearchAuditDto): Promise<any> {
const startTime = Date.now();
const result = await this.auditSearchService.search(dto);
this.auditMetricsService.recordSearchOperation(Date.now() - startTime);
return result;
}

/**
* Get audit logs for a specific entity
* GET /audit/entity/CLAIM/claim-id-123
*/
@Get('entity/:entityType/:entityId')
@ApiOperation({ summary: 'Get audit logs for a specific entity' })
async getEntityAuditLogs(
@Param('entityType') entityType: AuditEntityType,
@Param('entityId') entityId: string,
): Promise<AuditLog[]> {
return this.auditTrailService.getEntityAuditLogs(entityType, entityId);
}

/**
* Get audit logs for a specific user
* GET /audit/user/user-id-123?limit=50
*/
@Get('user/:userId')
@ApiOperation({ summary: 'Get audit logs for a specific user' })
async getUserAuditLogs(
@Param('userId') userId: string,
@Query('limit') limit?: string,
@Query('offset') offset?: string,
): Promise<{ logs: AuditLog[]; total: number }> {
const parsedLimit = limit ? Math.min(parseInt(limit, 10), 500) : 100;
const parsedOffset = offset ? Math.max(parseInt(offset, 10), 0) : 0;

return this.auditTrailService.getUserAuditLogs(
userId,
parsedLimit,
parsedOffset,
);
return this.auditTrailService.getUserAuditLogs(userId, parsedLimit, parsedOffset);
}

/**
* Get audit logs for a specific action type
* GET /audit/action/CLAIM_CREATED?limit=50
*/
@Get('action/:actionType')
@ApiOperation({ summary: 'Get audit logs by action type' })
async getActionAuditLogs(
@Param('actionType') actionType: AuditActionType,
@Query('limit') limit?: string,
@Query('offset') offset?: string,
): Promise<{ logs: AuditLog[]; total: number }> {
const parsedLimit = limit ? Math.min(parseInt(limit, 10), 500) : 100;
const parsedOffset = offset ? Math.max(parseInt(offset, 10), 0) : 0;

return this.auditTrailService.getActionAuditLogs(
actionType,
parsedLimit,
parsedOffset,
);
return this.auditTrailService.getActionAuditLogs(actionType, parsedLimit, parsedOffset);
}

/**
* Get change history for a specific entity
* GET /audit/changes/CLAIM/claim-id-123
*/
@Get('changes/:entityType/:entityId')
@ApiOperation({ summary: 'Get change history for a specific entity' })
async getChangeHistory(
@Param('entityType') entityType: AuditEntityType,
@Param('entityId') entityId: string,
Expand All @@ -103,16 +87,135 @@
return this.auditTrailService.getChangeHistory(entityType, entityId);
}

/**
* Get audit summary for a specific entity type
* GET /audit/summary?entityType=CLAIM&days=7
*/
@Get('summary')
@ApiOperation({ summary: 'Get audit summary grouped by action type' })
async getAuditSummary(
@Query('entityType') entityType?: AuditEntityType,
@Query('days') days?: string,
): Promise<Record<string, number>> {
const parsedDays = days ? Math.max(parseInt(days, 10), 1) : 7;
return this.auditTrailService.getAuditSummary(entityType, parsedDays);
}

@Get('correlation/:correlationId')
@ApiOperation({ summary: 'Get audit logs by correlation ID' })
async getByCorrelationId(
@Param('correlationId') correlationId: string,
): Promise<AuditLog[]> {
return this.auditSearchService.findByCorrelationId(correlationId);
}

@Get('request/:requestId')
@ApiOperation({ summary: 'Get audit logs by request ID' })
async getByRequestId(
@Param('requestId') requestId: string,
): Promise<AuditLog[]> {
return this.auditSearchService.findByRequestId(requestId);
}

@Get('failed-access')
@ApiOperation({ summary: 'Get failed access attempts' })
async getFailedAccess(
@Query('startDate') startDate?: string,
@Query('endDate') endDate?: string,
@Query('limit') limit?: string,
@Query('offset') offset?: string,
): Promise<{ logs: AuditLog[]; total: number }> {
return this.auditSearchService.findFailedAccessAttempts(
startDate ? new Date(startDate) : undefined,
endDate ? new Date(endDate) : undefined,
limit ? Math.min(parseInt(limit, 10), 500) : 100,
offset ? Math.max(parseInt(offset, 10), 0) : 0,
);
}

@Get('security-events')
@ApiOperation({ summary: 'Get security event audit logs' })
async getSecurityEvents(
@Query('startDate') startDate?: string,
@Query('endDate') endDate?: string,
@Query('limit') limit?: string,
@Query('offset') offset?: string,
): Promise<{ logs: AuditLog[]; total: number }> {
return this.auditSearchService.findSecurityEvents(
startDate ? new Date(startDate) : undefined,
endDate ? new Date(endDate) : undefined,
limit ? Math.min(parseInt(limit, 10), 500) : 100,
offset ? Math.max(parseInt(offset, 10), 0) : 0,
);
}

@Get('integrity/:id')
@ApiOperation({ summary: 'Verify integrity of an audit record' })
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(UserRole.ADMIN, UserRole.SUPER_ADMIN)
async verifyIntegrity(@Param('id') id: string): Promise<{ valid: boolean; id: string }> {
const result = await this.auditTrailService.verifyIntegrity(id);
return { valid: result.valid, id };
}

@Post('reports')
@ApiOperation({ summary: 'Generate a compliance report' })
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(UserRole.MODERATOR, UserRole.ADMIN, UserRole.SUPER_ADMIN)
async generateReport(
@Body() dto: GenerateReportDto,
@CurrentUser() user: any,
): Promise<any> {
this.auditMetricsService.incrementExportRequests();
return this.auditComplianceService.generateReport(dto, user?.id || 'system');
}

@Get('reports/types')
@ApiOperation({ summary: 'Get available report types' })
getReportTypes(): { types: string[] } {
return { types: Object.values(ReportType) };
}

@Get('metrics')
@ApiOperation({ summary: 'Get audit system metrics' })
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(UserRole.ADMIN, UserRole.SUPER_ADMIN)
async getMetrics(): Promise<any> {
return this.auditMetricsService.getMetrics();
}

@Get('retention')
@ApiOperation({ summary: 'Get retention policy status' })
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(UserRole.ADMIN, UserRole.SUPER_ADMIN)
async getRetentionStatus(): Promise<any> {
const status = await this.auditTrailService.getRetentionStatus();
const config = this.auditRetentionService.getRetentionConfig();
return { ...status, config };
}

@Post('legal-hold/:entityId')
@ApiOperation({ summary: 'Place legal hold on audit records for an entity' })
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(UserRole.ADMIN, UserRole.SUPER_ADMIN)
@ApiBody({ schema: { properties: { reason: { type: 'string' }, initiatedBy: { type: 'string' } } } })
async placeLegalHold(
@Param('entityId') entityId: string,
@Body('reason') reason: string,

Check warning on line 200 in src/audit/controllers/audit-log.controller.ts

View workflow job for this annotation

GitHub Actions / Lint

Unsafe argument of type `any` assigned to a parameter of type `string`
@CurrentUser() user: any,
): Promise<{ affected: number }> {
const affected = await this.auditTrailService.placeLegalHold(
entityId,
reason || 'Legal hold',
user?.id || 'system',
);
return { affected };
}

@Patch('legal-hold/:entityId/remove')
@ApiOperation({ summary: 'Remove legal hold from audit records' })
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(UserRole.ADMIN, UserRole.SUPER_ADMIN)
async removeLegalHold(
@Param('entityId') entityId: string,
): Promise<{ affected: number }> {
const affected = await this.auditTrailService.removeLegalHold(entityId);
return { affected };
}
}
70 changes: 70 additions & 0 deletions src/audit/dto/compliance-report.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsEnum, IsOptional, IsDateString, IsInt, Min } from 'class-validator';
import { Type } from 'class-transformer';

export enum ReportType {
ADMIN_ACTIVITY = 'ADMIN_ACTIVITY',
MODERATION_ACTIONS = 'MODERATION_ACTIONS',
GOVERNANCE_ACTIONS = 'GOVERNANCE_ACTIONS',
AUTHENTICATION_HISTORY = 'AUTHENTICATION_HISTORY',
FAILED_ACCESS_ATTEMPTS = 'FAILED_ACCESS_ATTEMPTS',
SECURITY_EVENTS = 'SECURITY_EVENTS',
PROTOCOL_OPERATIONS = 'PROTOCOL_OPERATIONS',
USER_ACTIVITY = 'USER_ACTIVITY',
}

export enum ReportFormat {
JSON = 'JSON',
CSV = 'CSV',
}

export class GenerateReportDto {
@ApiProperty({ enum: ReportType })
@IsEnum(ReportType)
reportType: ReportType;

@ApiPropertyOptional({ enum: ReportFormat, default: ReportFormat.JSON })
@IsOptional()
@IsEnum(ReportFormat)
format?: ReportFormat;

@ApiPropertyOptional()
@IsOptional()
@IsDateString()
startDate?: string;

@ApiPropertyOptional()
@IsOptional()
@IsDateString()
endDate?: string;

@ApiPropertyOptional()
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
days?: number;
}

export class ReportResponse {
@ApiProperty()
reportType: ReportType;

@ApiProperty()
generatedAt: string;

@ApiProperty()
period: { start: string; end: string };

@ApiProperty()
generatedBy: string;

@ApiProperty()
totalEvents: number;

@ApiProperty()
data: any;

@ApiProperty({ required: false })
exportUrl?: string;
}
Loading
Loading