Files
sfera-new/src/graphql/security/advanced-audit-reporting.ts
Veronika Smirnova d05f0a6a93 feat(security): обновление системы безопасности GraphQL и исправления ESLint
- Обновлены тесты безопасности для всех ролей (SELLER, WHOLESALE, FULFILLMENT, LOGIST)
- Улучшен мониторинг и аудит доступа к коммерческим данным
- Добавлена интеграция с внешними системами мониторинга
- Исправлены ESLint предупреждения в компонентах поставщика
- Обновлены middleware для безопасности GraphQL резолверов

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-23 18:49:32 +03:00

728 lines
22 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Расширенная система отчетности по аудиту безопасности
*
* Предоставляет детальные отчеты, аналитику и визуализацию
* активности пользователей в системе для выявления паттернов и угроз
*/
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<SecurityMetrics> {
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<Array<{ hour: number; count: bigint }>>`
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<UserAnalytics> {
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<OrganizationReport> {
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<SecurityTrends> {
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<number> {
try {
const hourlyData = await prisma.$queryRaw<Array<{ count: bigint }>>`
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<UserAnalytics['anomalies']> {
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<OrganizationReport['compliance']> {
// Анализ доступа к данным
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<Array<{ count: bigint }>>`
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,
}
}
}