/** * Расширенная система отчетности по аудиту безопасности * * Предоставляет детальные отчеты, аналитику и визуализацию * активности пользователей в системе для выявления паттернов и угроз */ import { PrismaClient } from '@prisma/client' import { SecurityLogger } from '../../lib/security-logger' import { CommercialAccessType, ResourceType, SecurityAlert } from './types' /** * Детальные метрики безопасности */ interface SecurityMetrics { totalAccesses: number uniqueUsers: number topActions: Array<{ action: string; count: number }> organizationBreakdown: Array<{ type: string; count: number }> timeDistribution: Array<{ hour: number; count: number }> suspiciousActivity: number resolvedAlerts: number activeAlerts: number } /** * Аналитика пользователя */ interface UserAnalytics { userId: string organizationType: string organizationId: string totalAccesses: number uniqueResources: number lastActivity: Date riskScore: number activities: Array<{ action: CommercialAccessType resourceType: ResourceType count: number avgPerHour: number maxPerHour: number timeRange: { start: Date; end: Date } }> anomalies: Array<{ type: 'VOLUME_SPIKE' | 'UNUSUAL_TIME' | 'NEW_RESOURCE' | 'RAPID_SUCCESSION' description: string severity: 'LOW' | 'MEDIUM' | 'HIGH' timestamp: Date }> } /** * Отчет по организации */ interface OrganizationReport { organizationId: string organizationType: string period: { start: Date; end: Date } users: number totalActivity: number breakdown: { viewing: number modifying: number exporting: number } compliance: { dataAccess: 'COMPLIANT' | 'CONCERNING' | 'VIOLATION' partnerships: 'VALID' | 'QUESTIONABLE' | 'INVALID' timePatterns: 'NORMAL' | 'UNUSUAL' } alerts: { generated: number resolved: number highPriority: number } } /** * Тренды безопасности */ interface SecurityTrends { period: { start: Date; end: Date } dataPoints: Array<{ timestamp: Date totalAccesses: number uniqueUsers: number alerts: number riskScore: number }> predictions: { nextPeriodRisk: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL' expectedVolume: number recommendedActions: string[] } } export class AdvancedAuditReporting { /** * Генерирует полный отчет по безопасности за период */ static async generateSecurityReport( prisma: PrismaClient, startDate: Date, endDate: Date, ): Promise { try { // Основные метрики const totalAccesses = await prisma.auditLog.count({ where: { timestamp: { gte: startDate, lte: endDate }, action: { startsWith: 'DATA_ACCESS:' }, }, }) const uniqueUsers = await prisma.auditLog .groupBy({ by: ['userId'], where: { timestamp: { gte: startDate, lte: endDate }, action: { startsWith: 'DATA_ACCESS:' }, }, }) .then((groups) => groups.length) // Топ действий const topActionsRaw = await prisma.auditLog.groupBy({ by: ['action'], where: { timestamp: { gte: startDate, lte: endDate }, action: { startsWith: 'DATA_ACCESS:' }, }, _count: { action: true }, orderBy: { _count: { action: 'desc' } }, take: 10, }) const topActions = topActionsRaw.map((item) => ({ action: item.action.replace('DATA_ACCESS:', ''), count: item._count.action, })) // Распределение по типам организаций const organizationBreakdownRaw = await prisma.auditLog.groupBy({ by: ['organizationType'], where: { timestamp: { gte: startDate, lte: endDate }, action: { startsWith: 'DATA_ACCESS:' }, }, _count: { organizationType: true }, }) const organizationBreakdown = organizationBreakdownRaw.map((item) => ({ type: item.organizationType || 'UNKNOWN', count: item._count.organizationType, })) // Распределение по времени (по часам) const timeDistributionRaw = await prisma.$queryRaw>` SELECT EXTRACT(HOUR FROM timestamp) as hour, COUNT(*) as count FROM AuditLog WHERE timestamp >= ${startDate} AND timestamp <= ${endDate} AND action LIKE 'DATA_ACCESS:%' GROUP BY EXTRACT(HOUR FROM timestamp) ORDER BY hour ` const timeDistribution = timeDistributionRaw.map((item) => ({ hour: Number(item.hour), count: Number(item.count), })) // Подозрительная активность const suspiciousActivity = await prisma.auditLog.count({ where: { timestamp: { gte: startDate, lte: endDate }, action: 'UNAUTHORIZED_ACCESS_ATTEMPT', }, }) // Алерты const resolvedAlerts = await prisma.securityAlert.count({ where: { timestamp: { gte: startDate, lte: endDate }, resolved: true, }, }) const activeAlerts = await prisma.securityAlert.count({ where: { timestamp: { gte: startDate, lte: endDate }, resolved: false, }, }) return { totalAccesses, uniqueUsers, topActions, organizationBreakdown, timeDistribution, suspiciousActivity, resolvedAlerts, activeAlerts, } } catch (error) { SecurityLogger.logSecurityError(error as Error, { operation: 'generateSecurityReport', period: { startDate, endDate }, }) throw error } } /** * Генерирует детальную аналитику пользователя */ static async generateUserAnalytics( prisma: PrismaClient, userId: string, period: Date = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), ): Promise { try { const user = await prisma.user.findUnique({ where: { id: userId }, include: { organization: true }, }) if (!user) { throw new Error(`User ${userId} not found`) } const since = period const now = new Date() // Основные метрики const totalAccesses = await prisma.auditLog.count({ where: { userId, timestamp: { gte: since }, action: { startsWith: 'DATA_ACCESS:' }, }, }) const uniqueResourcesRaw = await prisma.auditLog.groupBy({ by: ['resourceId'], where: { userId, timestamp: { gte: since }, action: { startsWith: 'DATA_ACCESS:' }, resourceId: { not: null }, }, }) const uniqueResources = uniqueResourcesRaw.length const lastActivityRaw = await prisma.auditLog.findFirst({ where: { userId, timestamp: { gte: since }, }, orderBy: { timestamp: 'desc' }, }) const lastActivity = lastActivityRaw?.timestamp || new Date(0) // Детальная активность по действиям const activitiesRaw = await prisma.auditLog.groupBy({ by: ['action', 'resourceType'], where: { userId, timestamp: { gte: since }, action: { startsWith: 'DATA_ACCESS:' }, }, _count: { action: true }, _min: { timestamp: true }, _max: { timestamp: true }, }) const activities = activitiesRaw.map((item) => { const hoursSinceStart = Math.max( 1, (now.getTime() - (item._min.timestamp?.getTime() || now.getTime())) / (1000 * 60 * 60), ) return { action: item.action.replace('DATA_ACCESS:', '') as CommercialAccessType, resourceType: item.resourceType as ResourceType, count: item._count.action, avgPerHour: item._count.action / hoursSinceStart, maxPerHour: await this.getMaxHourlyActivity(prisma, userId, item.action, since), timeRange: { start: item._min.timestamp || since, end: item._max.timestamp || now, }, } }) // Вычисление аномалий const anomalies = await this.detectUserAnomalies(prisma, userId, since, activities) // Расчет risk score const riskScore = this.calculateRiskScore(activities, anomalies, totalAccesses) return { userId, organizationType: user.organization?.type || 'UNKNOWN', organizationId: user.organizationId || '', totalAccesses, uniqueResources, lastActivity, riskScore, activities, anomalies, } } catch (error) { SecurityLogger.logSecurityError(error as Error, { operation: 'generateUserAnalytics', userId, period, }) throw error } } /** * Генерирует отчет по организации */ static async generateOrganizationReport( prisma: PrismaClient, organizationId: string, startDate: Date, endDate: Date, ): Promise { try { const organization = await prisma.organization.findUnique({ where: { id: organizationId }, include: { users: true }, }) if (!organization) { throw new Error(`Organization ${organizationId} not found`) } const userIds = organization.users.map((user) => user.id) // Основные метрики const totalActivity = await prisma.auditLog.count({ where: { userId: { in: userIds }, timestamp: { gte: startDate, lte: endDate }, action: { startsWith: 'DATA_ACCESS:' }, }, }) // Классификация активности const viewingActions = await prisma.auditLog.count({ where: { userId: { in: userIds }, timestamp: { gte: startDate, lte: endDate }, action: { in: ['DATA_ACCESS:VIEW_PRICE', 'DATA_ACCESS:VIEW_RECIPE', 'DATA_ACCESS:VIEW_CONTACTS'] }, }, }) const modifyingActions = await prisma.auditLog.count({ where: { userId: { in: userIds }, timestamp: { gte: startDate, lte: endDate }, action: { contains: 'UPDATE' }, }, }) const exportingActions = await prisma.auditLog.count({ where: { userId: { in: userIds }, timestamp: { gte: startDate, lte: endDate }, action: { contains: 'EXPORT' }, }, }) // Анализ соответствия const compliance = await this.analyzeOrganizationCompliance( prisma, organizationId, userIds, startDate, endDate, ) // Алерты const generatedAlerts = await prisma.securityAlert.count({ where: { userId: { in: userIds }, timestamp: { gte: startDate, lte: endDate }, }, }) const resolvedAlerts = await prisma.securityAlert.count({ where: { userId: { in: userIds }, timestamp: { gte: startDate, lte: endDate }, resolved: true, }, }) const highPriorityAlerts = await prisma.securityAlert.count({ where: { userId: { in: userIds }, timestamp: { gte: startDate, lte: endDate }, severity: { in: ['HIGH', 'CRITICAL'] }, }, }) return { organizationId, organizationType: organization.type, period: { start: startDate, end: endDate }, users: userIds.length, totalActivity, breakdown: { viewing: viewingActions, modifying: modifyingActions, exporting: exportingActions, }, compliance, alerts: { generated: generatedAlerts, resolved: resolvedAlerts, highPriority: highPriorityAlerts, }, } } catch (error) { SecurityLogger.logSecurityError(error as Error, { operation: 'generateOrganizationReport', organizationId, period: { startDate, endDate }, }) throw error } } /** * Анализирует тренды безопасности и делает прогнозы */ static async analyzeSecurityTrends( prisma: PrismaClient, days: number = 30, ): Promise { try { const endDate = new Date() const startDate = new Date(endDate.getTime() - days * 24 * 60 * 60 * 1000) // Получаем данные по дням const dailyDataRaw = await prisma.$queryRaw< Array<{ date: Date totalAccesses: bigint uniqueUsers: bigint alerts: bigint }> >` SELECT DATE(timestamp) as date, COUNT(CASE WHEN action LIKE 'DATA_ACCESS:%' THEN 1 END) as totalAccesses, COUNT(DISTINCT CASE WHEN action LIKE 'DATA_ACCESS:%' THEN userId END) as uniqueUsers, COUNT(CASE WHEN action = 'UNAUTHORIZED_ACCESS_ATTEMPT' THEN 1 END) as alerts FROM AuditLog WHERE timestamp >= ${startDate} AND timestamp <= ${endDate} GROUP BY DATE(timestamp) ORDER BY date ` const dataPoints = dailyDataRaw.map((item, index) => { // Простой расчет risk score на основе активности и алертов const totalAccesses = Number(item.totalAccesses) const alerts = Number(item.alerts) const riskScore = Math.min(100, (alerts * 20) + Math.max(0, totalAccesses - 1000) / 100) return { timestamp: item.date, totalAccesses, uniqueUsers: Number(item.uniqueUsers), alerts, riskScore, } }) // Простое прогнозирование на основе трендов const predictions = this.generateSecurityPredictions(dataPoints) return { period: { start: startDate, end: endDate }, dataPoints, predictions, } } catch (error) { SecurityLogger.logSecurityError(error as Error, { operation: 'analyzeSecurityTrends', days, }) throw error } } /** * Получает максимальную почасовую активность пользователя */ private static async getMaxHourlyActivity( prisma: PrismaClient, userId: string, action: string, since: Date, ): Promise { try { const hourlyData = await prisma.$queryRaw>` SELECT COUNT(*) as count FROM AuditLog WHERE userId = ${userId} AND action = ${action} AND timestamp >= ${since} GROUP BY DATE(timestamp), EXTRACT(HOUR FROM timestamp) ORDER BY count DESC LIMIT 1 ` return hourlyData.length > 0 ? Number(hourlyData[0].count) : 0 } catch (error) { return 0 } } /** * Обнаруживает аномалии в поведении пользователя */ private static async detectUserAnomalies( prisma: PrismaClient, userId: string, since: Date, activities: UserAnalytics['activities'], ): Promise { const anomalies: UserAnalytics['anomalies'] = [] for (const activity of activities) { // Аномалия объема - превышение нормальной активности в 3+ раза if (activity.maxPerHour > activity.avgPerHour * 3 && activity.avgPerHour > 10) { anomalies.push({ type: 'VOLUME_SPIKE', description: `Spike in ${activity.action} activity: ${activity.maxPerHour} per hour vs average ${activity.avgPerHour.toFixed(1)}`, severity: activity.maxPerHour > activity.avgPerHour * 5 ? 'HIGH' : 'MEDIUM', timestamp: activity.timeRange.end, }) } // Аномалия времени - активность в необычные часы (поздно ночью/рано утром) const nightActivity = await prisma.auditLog.count({ where: { userId, action: `DATA_ACCESS:${activity.action}`, timestamp: { gte: since }, }, }) const totalActivity = activity.count const nightRatio = nightActivity / totalActivity if (nightRatio > 0.3) { // Более 30% активности ночью anomalies.push({ type: 'UNUSUAL_TIME', description: `High night-time activity: ${(nightRatio * 100).toFixed(1)}% of ${activity.action} actions`, severity: nightRatio > 0.5 ? 'MEDIUM' : 'LOW', timestamp: activity.timeRange.end, }) } } return anomalies } /** * Вычисляет оценку риска пользователя */ private static calculateRiskScore( activities: UserAnalytics['activities'], anomalies: UserAnalytics['anomalies'], totalAccesses: number, ): number { let score = 0 // Базовый score от объема активности score += Math.min(30, totalAccesses / 100) // Score от аномалий anomalies.forEach((anomaly) => { switch (anomaly.severity) { case 'LOW': score += 5 break case 'MEDIUM': score += 15 break case 'HIGH': score += 30 break } }) // Score от разнообразия активности const uniqueActions = activities.length score += Math.min(20, uniqueActions * 3) return Math.min(100, Math.max(0, score)) } /** * Анализирует соответствие организации требованиям безопасности */ private static async analyzeOrganizationCompliance( prisma: PrismaClient, organizationId: string, userIds: string[], startDate: Date, endDate: Date, ): Promise { // Анализ доступа к данным const unauthorizedAttempts = await prisma.auditLog.count({ where: { userId: { in: userIds }, timestamp: { gte: startDate, lte: endDate }, action: 'UNAUTHORIZED_ACCESS_ATTEMPT', }, }) const dataAccess = unauthorizedAttempts === 0 ? 'COMPLIANT' : unauthorizedAttempts > 10 ? 'VIOLATION' : 'CONCERNING' // Анализ партнерств (упрощенный) const partnerships = 'VALID' // TODO: реализовать анализ партнерств // Анализ временных паттернов const nightActivity = await prisma.$queryRaw>` SELECT COUNT(*) as count FROM AuditLog WHERE userId IN (${userIds.join(',')}) AND timestamp >= ${startDate} AND timestamp <= ${endDate} AND EXTRACT(HOUR FROM timestamp) BETWEEN 0 AND 6 AND action LIKE 'DATA_ACCESS:%' ` const totalActivity = await prisma.auditLog.count({ where: { userId: { in: userIds }, timestamp: { gte: startDate, lte: endDate }, action: { startsWith: 'DATA_ACCESS:' }, }, }) const nightRatio = totalActivity > 0 ? Number(nightActivity[0]?.count || 0) / totalActivity : 0 const timePatterns = nightRatio > 0.2 ? 'UNUSUAL' : 'NORMAL' return { dataAccess, partnerships, timePatterns, } } /** * Генерирует прогнозы безопасности */ private static generateSecurityPredictions( dataPoints: SecurityTrends['dataPoints'], ): SecurityTrends['predictions'] { if (dataPoints.length < 7) { return { nextPeriodRisk: 'MEDIUM', expectedVolume: 0, recommendedActions: ['Insufficient data for prediction'], } } // Анализ трендов за последнюю неделю const recentPoints = dataPoints.slice(-7) const avgRiskScore = recentPoints.reduce((sum, p) => sum + p.riskScore, 0) / recentPoints.length const avgVolume = recentPoints.reduce((sum, p) => sum + p.totalAccesses, 0) / recentPoints.length // Расчет тренда const firstHalf = recentPoints.slice(0, 3) const secondHalf = recentPoints.slice(-3) const firstAvgRisk = firstHalf.reduce((sum, p) => sum + p.riskScore, 0) / firstHalf.length const secondAvgRisk = secondHalf.reduce((sum, p) => sum + p.riskScore, 0) / secondHalf.length const riskTrend = secondAvgRisk - firstAvgRisk // Определение уровня риска let nextPeriodRisk: SecurityTrends['predictions']['nextPeriodRisk'] if (avgRiskScore > 70 || riskTrend > 20) { nextPeriodRisk = 'CRITICAL' } else if (avgRiskScore > 40 || riskTrend > 10) { nextPeriodRisk = 'HIGH' } else if (avgRiskScore > 20 || riskTrend > 5) { nextPeriodRisk = 'MEDIUM' } else { nextPeriodRisk = 'LOW' } // Рекомендации const recommendedActions: string[] = [] if (riskTrend > 10) { recommendedActions.push('Увеличить мониторинг активности пользователей') } if (avgRiskScore > 50) { recommendedActions.push('Провести аудит учетных записей с высоким риском') } if (recentPoints.some((p) => p.alerts > 5)) { recommendedActions.push('Усилить меры безопасности для критичных ресурсов') } return { nextPeriodRisk, expectedVolume: Math.round(avgVolume * 1.1), // Прогноз роста на 10% recommendedActions, } } }