2026-03-19 01:39:34 +08:00
|
|
|
import { Injectable, Logger } from '@nestjs/common';
|
|
|
|
|
import { ConfigService } from '@nestjs/config';
|
|
|
|
|
import { RedisService } from '../cache/RedisService';
|
|
|
|
|
|
|
|
|
|
export interface SecurityAuditLog {
|
|
|
|
|
id: string;
|
|
|
|
|
timestamp: string;
|
|
|
|
|
userId?: string;
|
|
|
|
|
tenantId?: string;
|
|
|
|
|
action: string;
|
|
|
|
|
resource: string;
|
|
|
|
|
details: any;
|
|
|
|
|
ipAddress?: string;
|
|
|
|
|
userAgent?: string;
|
|
|
|
|
severity: 'low' | 'medium' | 'high' | 'critical';
|
|
|
|
|
status: 'success' | 'failure' | 'blocked';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface SecurityAlert {
|
|
|
|
|
id: string;
|
|
|
|
|
timestamp: string;
|
|
|
|
|
type: string;
|
|
|
|
|
severity: 'low' | 'medium' | 'high' | 'critical';
|
|
|
|
|
title: string;
|
|
|
|
|
description: string;
|
|
|
|
|
details: any;
|
|
|
|
|
affectedResources: string[];
|
|
|
|
|
recommendations: string[];
|
|
|
|
|
status: 'open' | 'investigating' | 'resolved' | 'false_positive';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface SecurityMetrics {
|
|
|
|
|
timestamp: string;
|
|
|
|
|
totalRequests: number;
|
|
|
|
|
blockedRequests: number;
|
|
|
|
|
failedAuthAttempts: number;
|
|
|
|
|
suspiciousActivities: number;
|
|
|
|
|
vulnerabilitiesFound: number;
|
|
|
|
|
complianceScore: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Injectable()
|
|
|
|
|
export class SecurityHardeningService {
|
|
|
|
|
private readonly logger = new Logger(SecurityHardeningService.name);
|
|
|
|
|
private auditLogs: SecurityAuditLog[] = [];
|
|
|
|
|
private securityAlerts: SecurityAlert[] = [];
|
|
|
|
|
private readonly maxAuditLogs = 10000;
|
|
|
|
|
private securityCheckInterval: NodeJS.Timeout;
|
|
|
|
|
|
|
|
|
|
constructor(
|
|
|
|
|
private readonly configService: ConfigService,
|
|
|
|
|
private readonly redisService: RedisService,
|
|
|
|
|
) {}
|
|
|
|
|
|
|
|
|
|
async initialize(): Promise<void> {
|
|
|
|
|
this.logger.log('🚀 Initializing Security Hardening Service...');
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await this.setupSecurityMonitoring();
|
|
|
|
|
await this.initializeSecurityPolicies();
|
|
|
|
|
await this.startSecurityChecks();
|
|
|
|
|
await this.setupVulnerabilityScanning();
|
|
|
|
|
|
|
|
|
|
this.logger.log('✅ Security Hardening Service initialized successfully');
|
|
|
|
|
} catch (error) {
|
|
|
|
|
this.logger.error('❌ Failed to initialize Security Hardening Service', error);
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async setupSecurityMonitoring(): Promise<void> {
|
|
|
|
|
this.logger.log('🔒 Setting up security monitoring...');
|
|
|
|
|
|
|
|
|
|
const monitoringInterval = this.configService.get('SECURITY_MONITORING_INTERVAL', 60000);
|
|
|
|
|
|
|
|
|
|
this.securityCheckInterval = setInterval(async () => {
|
|
|
|
|
await this.performSecurityChecks();
|
|
|
|
|
await this.analyzeSecurityMetrics();
|
|
|
|
|
await this.generateSecurityAlerts();
|
|
|
|
|
}, monitoringInterval);
|
|
|
|
|
|
|
|
|
|
await this.performSecurityChecks();
|
|
|
|
|
this.logger.log('✅ Security monitoring setup completed');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async initializeSecurityPolicies(): Promise<void> {
|
|
|
|
|
this.logger.log('📋 Initializing security policies...');
|
|
|
|
|
|
|
|
|
|
await this.configureRBAC();
|
|
|
|
|
await this.configureRateLimiting();
|
|
|
|
|
await this.configureInputValidation();
|
|
|
|
|
await this.configureOutputEncoding();
|
|
|
|
|
await this.configureSessionSecurity();
|
|
|
|
|
await this.configureCSRFProtection();
|
|
|
|
|
await this.configureSecurityHeaders();
|
|
|
|
|
await this.configureDataEncryption();
|
|
|
|
|
|
|
|
|
|
this.logger.log('✅ Security policies initialized');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async configureRBAC(): Promise<void> {
|
|
|
|
|
this.logger.log('👤 Configuring RBAC...');
|
|
|
|
|
|
|
|
|
|
const roles = {
|
|
|
|
|
ADMIN: {
|
|
|
|
|
permissions: ['*'],
|
|
|
|
|
description: 'Full system access'
|
|
|
|
|
},
|
|
|
|
|
MANAGER: {
|
|
|
|
|
permissions: [
|
|
|
|
|
'products:*',
|
|
|
|
|
'orders:*',
|
|
|
|
|
'reports:view',
|
|
|
|
|
'analytics:view',
|
|
|
|
|
'users:view',
|
|
|
|
|
'settings:manage'
|
|
|
|
|
],
|
|
|
|
|
description: 'Operational management'
|
|
|
|
|
},
|
|
|
|
|
OPERATOR: {
|
|
|
|
|
permissions: [
|
|
|
|
|
'products:view',
|
|
|
|
|
'products:create',
|
|
|
|
|
'orders:view',
|
|
|
|
|
'orders:update',
|
|
|
|
|
'reports:view'
|
|
|
|
|
],
|
|
|
|
|
description: 'Daily operations'
|
|
|
|
|
},
|
|
|
|
|
FINANCE: {
|
|
|
|
|
permissions: [
|
|
|
|
|
'billing:*',
|
|
|
|
|
'invoices:*',
|
|
|
|
|
'settlements:*',
|
|
|
|
|
'reports:view',
|
|
|
|
|
'analytics:view'
|
|
|
|
|
],
|
|
|
|
|
description: 'Financial operations'
|
|
|
|
|
},
|
|
|
|
|
SOURCING: {
|
|
|
|
|
permissions: [
|
|
|
|
|
'products:*',
|
|
|
|
|
'suppliers:*',
|
|
|
|
|
'orders:view',
|
|
|
|
|
'reports:view'
|
|
|
|
|
],
|
|
|
|
|
description: 'Product sourcing'
|
|
|
|
|
},
|
|
|
|
|
LOGISTICS: {
|
|
|
|
|
permissions: [
|
|
|
|
|
'orders:view',
|
|
|
|
|
'orders:update',
|
|
|
|
|
'shipments:*',
|
|
|
|
|
'tracking:*',
|
|
|
|
|
'reports:view'
|
|
|
|
|
],
|
|
|
|
|
description: 'Logistics management'
|
|
|
|
|
},
|
|
|
|
|
ANALYST: {
|
|
|
|
|
permissions: [
|
|
|
|
|
'reports:view',
|
|
|
|
|
'analytics:view',
|
|
|
|
|
'data:view',
|
|
|
|
|
'exports:*'
|
|
|
|
|
],
|
|
|
|
|
description: 'Data analysis'
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
await this.redisService.set('security:rbac:roles', JSON.stringify(roles));
|
|
|
|
|
this.logger.log('✅ RBAC configured with 7 roles');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async configureRateLimiting(): Promise<void> {
|
|
|
|
|
this.logger.log('⚡ Configuring rate limiting...');
|
|
|
|
|
|
|
|
|
|
const rateLimits = {
|
|
|
|
|
default: {
|
|
|
|
|
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
|
|
|
max: 100,
|
|
|
|
|
message: 'Too many requests from this IP'
|
|
|
|
|
},
|
|
|
|
|
auth: {
|
|
|
|
|
windowMs: 15 * 60 * 1000,
|
|
|
|
|
max: 5,
|
|
|
|
|
message: 'Too many authentication attempts'
|
|
|
|
|
},
|
|
|
|
|
api: {
|
|
|
|
|
windowMs: 1 * 60 * 1000, // 1 minute
|
|
|
|
|
max: 60,
|
|
|
|
|
message: 'API rate limit exceeded'
|
|
|
|
|
},
|
|
|
|
|
upload: {
|
|
|
|
|
windowMs: 1 * 60 * 1000,
|
|
|
|
|
max: 10,
|
|
|
|
|
message: 'Upload rate limit exceeded'
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
await this.redisService.set('security:ratelimits', JSON.stringify(rateLimits));
|
|
|
|
|
this.logger.log('✅ Rate limiting configured');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async configureInputValidation(): Promise<void> {
|
|
|
|
|
this.logger.log('🔍 Configuring input validation...');
|
|
|
|
|
|
|
|
|
|
const validationRules = {
|
|
|
|
|
email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
|
|
|
|
|
password: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/,
|
|
|
|
|
phone: /^\+?[\d\s-()]+$/,
|
|
|
|
|
url: /^https?:\/\/.+/,
|
|
|
|
|
numeric: /^\d+$/,
|
|
|
|
|
alphanumeric: /^[a-zA-Z0-9]+$/,
|
|
|
|
|
safeString: /^[a-zA-Z0-9\s\-_.,!?]+$/
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
await this.redisService.set('security:validation:rules', JSON.stringify(validationRules));
|
|
|
|
|
this.logger.log('✅ Input validation configured');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async configureOutputEncoding(): Promise<void> {
|
|
|
|
|
this.logger.log('🔒 Configuring output encoding...');
|
|
|
|
|
|
|
|
|
|
const encodingSettings = {
|
|
|
|
|
html: true,
|
|
|
|
|
js: true,
|
|
|
|
|
css: true,
|
|
|
|
|
url: true,
|
|
|
|
|
json: true
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
await this.redisService.set('security:encoding', JSON.stringify(encodingSettings));
|
|
|
|
|
this.logger.log('✅ Output encoding configured');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async configureSessionSecurity(): Promise<void> {
|
|
|
|
|
this.logger.log('🔐 Configuring session security...');
|
|
|
|
|
|
|
|
|
|
const sessionSettings = {
|
|
|
|
|
secret: this.configService.get('SESSION_SECRET', 'change-me-in-production'),
|
|
|
|
|
resave: false,
|
|
|
|
|
saveUninitialized: false,
|
|
|
|
|
cookie: {
|
|
|
|
|
secure: true,
|
|
|
|
|
httpOnly: true,
|
|
|
|
|
sameSite: 'strict',
|
|
|
|
|
maxAge: 24 * 60 * 60 * 1000 // 24 hours
|
|
|
|
|
},
|
|
|
|
|
rolling: true,
|
|
|
|
|
name: 'sessionId'
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
await this.redisService.set('security:session', JSON.stringify(sessionSettings));
|
|
|
|
|
this.logger.log('✅ Session security configured');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async configureCSRFProtection(): Promise<void> {
|
|
|
|
|
this.logger.log('🛡️ Configuring CSRF protection...');
|
|
|
|
|
|
|
|
|
|
const csrfSettings = {
|
|
|
|
|
enabled: true,
|
|
|
|
|
secretLength: 32,
|
|
|
|
|
saltLength: 16,
|
|
|
|
|
cookieOptions: {
|
|
|
|
|
httpOnly: true,
|
|
|
|
|
secure: true,
|
|
|
|
|
sameSite: 'strict'
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
await this.redisService.set('security:csrf', JSON.stringify(csrfSettings));
|
|
|
|
|
this.logger.log('✅ CSRF protection configured');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async configureSecurityHeaders(): Promise<void> {
|
|
|
|
|
this.logger.log('📋 Configuring security headers...');
|
|
|
|
|
|
|
|
|
|
const headers = {
|
|
|
|
|
'Strict-Transport-Security': 'max-age=31536000; includeSubDomains; preload',
|
|
|
|
|
'X-Content-Type-Options': 'nosniff',
|
|
|
|
|
'X-Frame-Options': 'DENY',
|
|
|
|
|
'X-XSS-Protection': '1; mode=block',
|
|
|
|
|
'Content-Security-Policy': "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:;",
|
|
|
|
|
'Referrer-Policy': 'strict-origin-when-cross-origin',
|
|
|
|
|
'Permissions-Policy': 'geolocation=(), microphone=(), camera=()',
|
|
|
|
|
'X-Download-Options': 'noopen',
|
|
|
|
|
'X-Permitted-Cross-Domain-Policies': 'none'
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
await this.redisService.set('security:headers', JSON.stringify(headers));
|
|
|
|
|
this.logger.log('✅ Security headers configured');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async configureDataEncryption(): Promise<void> {
|
|
|
|
|
this.logger.log('🔐 Configuring data encryption...');
|
|
|
|
|
|
|
|
|
|
const encryptionSettings = {
|
|
|
|
|
algorithm: 'aes-256-gcm',
|
|
|
|
|
keyLength: 32,
|
|
|
|
|
ivLength: 16,
|
|
|
|
|
authTagLength: 16,
|
|
|
|
|
encoding: 'base64'
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
await this.redisService.set('security:encryption', JSON.stringify(encryptionSettings));
|
|
|
|
|
this.logger.log('✅ Data encryption configured');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async startSecurityChecks(): Promise<void> {
|
|
|
|
|
this.logger.log('🔄 Starting security checks...');
|
|
|
|
|
|
|
|
|
|
const checkInterval = this.configService.get('SECURITY_CHECK_INTERVAL', 300000);
|
|
|
|
|
|
|
|
|
|
setInterval(async () => {
|
|
|
|
|
await this.performSecurityChecks();
|
|
|
|
|
await this.analyzeSecurityMetrics();
|
|
|
|
|
await this.generateSecurityAlerts();
|
|
|
|
|
}, checkInterval);
|
|
|
|
|
|
|
|
|
|
this.logger.log(`✅ Security checks started: ${checkInterval}ms interval`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async setupVulnerabilityScanning(): Promise<void> {
|
|
|
|
|
this.logger.log('🔍 Setting up vulnerability scanning...');
|
|
|
|
|
|
|
|
|
|
const scanInterval = this.configService.get('VULNERABILITY_SCAN_INTERVAL', 86400000); // 24 hours
|
|
|
|
|
|
|
|
|
|
setInterval(async () => {
|
|
|
|
|
await this.scanVulnerabilities();
|
|
|
|
|
await this.checkDependencies();
|
|
|
|
|
await this.analyzeCodeSecurity();
|
|
|
|
|
}, scanInterval);
|
|
|
|
|
|
|
|
|
|
this.logger.log(`✅ Vulnerability scanning started: ${scanInterval}ms interval`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async performSecurityChecks(): Promise<void> {
|
|
|
|
|
this.logger.log('🔍 Performing security checks...');
|
|
|
|
|
|
|
|
|
|
const checks = {
|
|
|
|
|
sqlInjection: await this.checkSQLInjection(),
|
|
|
|
|
xss: await this.checkXSS(),
|
|
|
|
|
csrf: await this.checkCSRF(),
|
|
|
|
|
authentication: await this.checkAuthentication(),
|
|
|
|
|
authorization: await this.checkAuthorization(),
|
|
|
|
|
dataValidation: await this.checkDataValidation(),
|
|
|
|
|
encryption: await this.checkEncryption(),
|
|
|
|
|
sessionSecurity: await this.checkSessionSecurity()
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
await this.cacheSecurityChecks(checks);
|
|
|
|
|
this.logger.log('✅ Security checks completed');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async checkSQLInjection(): Promise<boolean> {
|
|
|
|
|
try {
|
|
|
|
|
const patterns = [
|
|
|
|
|
/('|(\\')|(;)|(\-\-)|(\s+or\s+)|(\s+and\s+)/i,
|
|
|
|
|
/(union\s+select)|(drop\s+table)|(delete\s+from)|(insert\s+into)/i,
|
|
|
|
|
/(exec\s*\()|(execute\s*\()|(sp_executesql)/i
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
await this.redisService.set('security:check:sqlinjection', JSON.stringify({
|
|
|
|
|
status: 'passed',
|
|
|
|
|
patterns: patterns.length,
|
|
|
|
|
timestamp: new Date().toISOString()
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
this.logger.warn('SQL injection check failed', error);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async checkXSS(): Promise<boolean> {
|
|
|
|
|
try {
|
|
|
|
|
const patterns = [
|
|
|
|
|
/<script[^>]*>.*?<\/script>/gi,
|
|
|
|
|
/javascript:/gi,
|
|
|
|
|
/on\w+\s*=/gi,
|
|
|
|
|
/<iframe[^>]*>/gi,
|
|
|
|
|
/<object[^>]*>/gi,
|
|
|
|
|
/<embed[^>]*>/gi
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
await this.redisService.set('security:check:xss', JSON.stringify({
|
|
|
|
|
status: 'passed',
|
|
|
|
|
patterns: patterns.length,
|
|
|
|
|
timestamp: new Date().toISOString()
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
this.logger.warn('XSS check failed', error);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async checkCSRF(): Promise<boolean> {
|
|
|
|
|
try {
|
|
|
|
|
const csrfEnabled = await this.redisService.get('security:csrf');
|
|
|
|
|
const csrfStatus = csrfEnabled ? JSON.parse(csrfEnabled).enabled : false;
|
|
|
|
|
|
|
|
|
|
await this.redisService.set('security:check:csrf', JSON.stringify({
|
|
|
|
|
status: csrfStatus ? 'passed' : 'failed',
|
|
|
|
|
enabled: csrfStatus,
|
|
|
|
|
timestamp: new Date().toISOString()
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
return csrfStatus;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
this.logger.warn('CSRF check failed', error);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async checkAuthentication(): Promise<boolean> {
|
|
|
|
|
try {
|
|
|
|
|
const authSettings = await this.redisService.get('security:rbac:roles');
|
|
|
|
|
const hasAuth = authSettings !== null;
|
|
|
|
|
|
|
|
|
|
await this.redisService.set('security:check:authentication', JSON.stringify({
|
|
|
|
|
status: hasAuth ? 'passed' : 'failed',
|
|
|
|
|
hasAuthentication: hasAuth,
|
|
|
|
|
timestamp: new Date().toISOString()
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
return hasAuth;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
this.logger.warn('Authentication check failed', error);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async checkAuthorization(): Promise<boolean> {
|
|
|
|
|
try {
|
|
|
|
|
const rbacSettings = await this.redisService.get('security:rbac:roles');
|
|
|
|
|
const hasRBAC = rbacSettings !== null;
|
|
|
|
|
|
|
|
|
|
await this.redisService.set('security:check:authorization', JSON.stringify({
|
|
|
|
|
status: hasRBAC ? 'passed' : 'failed',
|
|
|
|
|
hasRBAC: hasRBAC,
|
|
|
|
|
timestamp: new Date().toISOString()
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
return hasRBAC;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
this.logger.warn('Authorization check failed', error);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async checkDataValidation(): Promise<boolean> {
|
|
|
|
|
try {
|
|
|
|
|
const validationRules = await this.redisService.get('security:validation:rules');
|
|
|
|
|
const hasValidation = validationRules !== null;
|
|
|
|
|
|
|
|
|
|
await this.redisService.set('security:check:validation', JSON.stringify({
|
|
|
|
|
status: hasValidation ? 'passed' : 'failed',
|
|
|
|
|
hasValidation: hasValidation,
|
|
|
|
|
timestamp: new Date().toISOString()
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
return hasValidation;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
this.logger.warn('Data validation check failed', error);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async checkEncryption(): Promise<boolean> {
|
|
|
|
|
try {
|
|
|
|
|
const encryptionSettings = await this.redisService.get('security:encryption');
|
|
|
|
|
const hasEncryption = encryptionSettings !== null;
|
|
|
|
|
|
|
|
|
|
await this.redisService.set('security:check:encryption', JSON.stringify({
|
|
|
|
|
status: hasEncryption ? 'passed' : 'failed',
|
|
|
|
|
hasEncryption: hasEncryption,
|
|
|
|
|
timestamp: new Date().toISOString()
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
return hasEncryption;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
this.logger.warn('Encryption check failed', error);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async checkSessionSecurity(): Promise<boolean> {
|
|
|
|
|
try {
|
|
|
|
|
const sessionSettings = await this.redisService.get('security:session');
|
|
|
|
|
const hasSessionSecurity = sessionSettings !== null;
|
|
|
|
|
|
|
|
|
|
await this.redisService.set('security:check:session', JSON.stringify({
|
|
|
|
|
status: hasSessionSecurity ? 'passed' : 'failed',
|
|
|
|
|
hasSessionSecurity: hasSessionSecurity,
|
|
|
|
|
timestamp: new Date().toISOString()
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
return hasSessionSecurity;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
this.logger.warn('Session security check failed', error);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async cacheSecurityChecks(checks: any): Promise<void> {
|
|
|
|
|
try {
|
|
|
|
|
await this.redisService.set(
|
|
|
|
|
'security:checks:latest',
|
|
|
|
|
JSON.stringify(checks),
|
|
|
|
|
'EX',
|
|
|
|
|
3600
|
|
|
|
|
);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
this.logger.warn('Failed to cache security checks', error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async analyzeSecurityMetrics(): Promise<SecurityMetrics> {
|
|
|
|
|
const metrics: SecurityMetrics = {
|
|
|
|
|
timestamp: new Date().toISOString(),
|
|
|
|
|
totalRequests: await this.getTotalRequests(),
|
|
|
|
|
blockedRequests: await this.getBlockedRequests(),
|
|
|
|
|
failedAuthAttempts: await this.getFailedAuthAttempts(),
|
|
|
|
|
suspiciousActivities: await this.getSuspiciousActivities(),
|
|
|
|
|
vulnerabilitiesFound: await this.getVulnerabilitiesFound(),
|
|
|
|
|
complianceScore: await this.calculateComplianceScore()
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
await this.cacheSecurityMetrics(metrics);
|
|
|
|
|
return metrics;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async getTotalRequests(): Promise<number> {
|
|
|
|
|
try {
|
|
|
|
|
const count = await this.redisService.get('security:metrics:requests');
|
|
|
|
|
return count ? parseInt(count) : 0;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async getBlockedRequests(): Promise<number> {
|
|
|
|
|
try {
|
|
|
|
|
const count = await this.redisService.get('security:metrics:blocked');
|
|
|
|
|
return count ? parseInt(count) : 0;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async getFailedAuthAttempts(): Promise<number> {
|
|
|
|
|
try {
|
|
|
|
|
const count = await this.redisService.get('security:metrics:failedAuth');
|
|
|
|
|
return count ? parseInt(count) : 0;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async getSuspiciousActivities(): Promise<number> {
|
|
|
|
|
try {
|
|
|
|
|
const count = await this.redisService.get('security:metrics:suspicious');
|
|
|
|
|
return count ? parseInt(count) : 0;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async getVulnerabilitiesFound(): Promise<number> {
|
|
|
|
|
try {
|
|
|
|
|
const count = await this.redisService.get('security:metrics:vulnerabilities');
|
|
|
|
|
return count ? parseInt(count) : 0;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async calculateComplianceScore(): Promise<number> {
|
|
|
|
|
try {
|
|
|
|
|
const checks = await this.redisService.get('security:checks:latest');
|
|
|
|
|
if (!checks) return 0;
|
|
|
|
|
|
|
|
|
|
const checkResults = JSON.parse(checks);
|
|
|
|
|
const passedChecks = Object.values(checkResults).filter((check: any) => check.status === 'passed').length;
|
|
|
|
|
const totalChecks = Object.keys(checkResults).length;
|
|
|
|
|
|
|
|
|
|
return Math.round((passedChecks / totalChecks) * 100);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async cacheSecurityMetrics(metrics: SecurityMetrics): Promise<void> {
|
|
|
|
|
try {
|
|
|
|
|
await this.redisService.set(
|
|
|
|
|
'security:metrics:latest',
|
|
|
|
|
JSON.stringify(metrics),
|
|
|
|
|
'EX',
|
|
|
|
|
3600
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
await this.redisService.lpush(
|
|
|
|
|
'security:metrics:history',
|
|
|
|
|
JSON.stringify(metrics)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
await this.redisService.ltrim('security:metrics:history', 0, 999);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
this.logger.warn('Failed to cache security metrics', error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async generateSecurityAlerts(): Promise<void> {
|
|
|
|
|
const metrics = await this.analyzeSecurityMetrics();
|
|
|
|
|
const alerts: SecurityAlert[] = [];
|
|
|
|
|
|
|
|
|
|
if (metrics.failedAuthAttempts > 10) {
|
|
|
|
|
alerts.push({
|
|
|
|
|
id: `alert-${Date.now()}-auth`,
|
|
|
|
|
timestamp: new Date().toISOString(),
|
|
|
|
|
type: 'authentication',
|
|
|
|
|
severity: 'high',
|
|
|
|
|
title: 'High Failed Authentication Attempts',
|
|
|
|
|
description: `${metrics.failedAuthAttempts} failed authentication attempts detected`,
|
|
|
|
|
details: { failedAttempts: metrics.failedAuthAttempts },
|
|
|
|
|
affectedResources: ['authentication'],
|
|
|
|
|
recommendations: [
|
|
|
|
|
'Review authentication logs',
|
|
|
|
|
'Implement account lockout',
|
|
|
|
|
'Notify affected users'
|
|
|
|
|
],
|
|
|
|
|
status: 'open'
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (metrics.suspiciousActivities > 5) {
|
|
|
|
|
alerts.push({
|
|
|
|
|
id: `alert-${Date.now()}-suspicious`,
|
|
|
|
|
timestamp: new Date().toISOString(),
|
|
|
|
|
type: 'suspicious_activity',
|
|
|
|
|
severity: 'medium',
|
|
|
|
|
title: 'Suspicious Activities Detected',
|
|
|
|
|
description: `${metrics.suspiciousActivities} suspicious activities detected`,
|
|
|
|
|
details: { suspiciousActivities: metrics.suspiciousActivities },
|
|
|
|
|
affectedResources: ['system'],
|
|
|
|
|
recommendations: [
|
|
|
|
|
'Review activity logs',
|
|
|
|
|
'Investigate suspicious patterns',
|
|
|
|
|
'Enhance monitoring'
|
|
|
|
|
],
|
|
|
|
|
status: 'open'
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (metrics.vulnerabilitiesFound > 0) {
|
|
|
|
|
alerts.push({
|
|
|
|
|
id: `alert-${Date.now()}-vulnerability`,
|
|
|
|
|
timestamp: new Date().toISOString(),
|
|
|
|
|
type: 'vulnerability',
|
|
|
|
|
severity: 'high',
|
|
|
|
|
title: 'Vulnerabilities Found',
|
|
|
|
|
description: `${metrics.vulnerabilitiesFound} vulnerabilities detected`,
|
|
|
|
|
details: { vulnerabilities: metrics.vulnerabilitiesFound },
|
|
|
|
|
affectedResources: ['system', 'dependencies'],
|
|
|
|
|
recommendations: [
|
|
|
|
|
'Review vulnerability report',
|
|
|
|
|
'Update affected dependencies',
|
|
|
|
|
'Apply security patches'
|
|
|
|
|
],
|
|
|
|
|
status: 'open'
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (metrics.complianceScore < 80) {
|
|
|
|
|
alerts.push({
|
|
|
|
|
id: `alert-${Date.now()}-compliance`,
|
|
|
|
|
timestamp: new Date().toISOString(),
|
|
|
|
|
type: 'compliance',
|
|
|
|
|
severity: 'medium',
|
|
|
|
|
title: 'Low Compliance Score',
|
|
|
|
|
description: `Compliance score is ${metrics.complianceScore}%`,
|
|
|
|
|
details: { complianceScore: metrics.complianceScore },
|
|
|
|
|
affectedResources: ['security_policies'],
|
|
|
|
|
recommendations: [
|
|
|
|
|
'Review security policies',
|
|
|
|
|
'Implement missing security measures',
|
|
|
|
|
'Update security configurations'
|
|
|
|
|
],
|
|
|
|
|
status: 'open'
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (alerts.length > 0) {
|
|
|
|
|
await this.cacheSecurityAlerts(alerts);
|
|
|
|
|
this.securityAlerts.push(...alerts);
|
|
|
|
|
this.logger.warn(`⚠️ ${alerts.length} security alerts generated`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async cacheSecurityAlerts(alerts: SecurityAlert[]): Promise<void> {
|
|
|
|
|
try {
|
|
|
|
|
for (const alert of alerts) {
|
|
|
|
|
await this.redisService.set(
|
|
|
|
|
`security:alert:${alert.id}`,
|
|
|
|
|
JSON.stringify(alert),
|
|
|
|
|
'EX',
|
|
|
|
|
86400
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await this.redisService.lpush(
|
|
|
|
|
'security:alerts:latest',
|
|
|
|
|
JSON.stringify(alerts)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
await this.redisService.ltrim('security:alerts:latest', 0, 99);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
this.logger.warn('Failed to cache security alerts', error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async scanVulnerabilities(): Promise<void> {
|
|
|
|
|
this.logger.log('🔍 Scanning for vulnerabilities...');
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const vulnerabilities = {
|
|
|
|
|
dependencies: await this.scanDependencies(),
|
|
|
|
|
code: await this.scanCodeVulnerabilities(),
|
|
|
|
|
configuration: await this.scanConfigurationVulnerabilities()
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const totalVulnerabilities =
|
|
|
|
|
vulnerabilities.dependencies.length +
|
|
|
|
|
vulnerabilities.code.length +
|
|
|
|
|
vulnerabilities.configuration.length;
|
|
|
|
|
|
|
|
|
|
await this.redisService.set(
|
|
|
|
|
'security:vulnerabilities:latest',
|
|
|
|
|
JSON.stringify(vulnerabilities),
|
|
|
|
|
'EX',
|
|
|
|
|
86400
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
await this.redisService.set(
|
|
|
|
|
'security:metrics:vulnerabilities',
|
|
|
|
|
totalVulnerabilities.toString()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
this.logger.log(`✅ Vulnerability scan completed: ${totalVulnerabilities} found`);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
this.logger.warn('Vulnerability scan failed', error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async scanDependencies(): Promise<any[]> {
|
|
|
|
|
try {
|
|
|
|
|
const output = require('child_process').execSync('npm audit --json', {
|
|
|
|
|
encoding: 'utf8'
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const results = JSON.parse(output);
|
|
|
|
|
const vulnerabilities = results.vulnerabilities || {};
|
|
|
|
|
|
|
|
|
|
return Object.entries(vulnerabilities).map(([name, vulns]: [string, any]) => ({
|
|
|
|
|
type: 'dependency',
|
|
|
|
|
name,
|
|
|
|
|
severity: vulns[0]?.severity || 'unknown',
|
|
|
|
|
count: vulns.length
|
|
|
|
|
}));
|
|
|
|
|
} catch (error) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-20 17:53:46 +08:00
|
|
|
private async scanCodeVulnerabilities(): Promise<Array<{ severity: string; description: string; location?: string }>> {
|
|
|
|
|
const vulnerabilities: Array<{ severity: string; description: string; location?: string }> = [];
|
2026-03-19 01:39:34 +08:00
|
|
|
|
|
|
|
|
const sensitivePatterns = [
|
|
|
|
|
{ pattern: /password\s*=\s*['"][^'"]+['"]/gi, severity: 'high', description: 'Hardcoded password' },
|
|
|
|
|
{ pattern: /api[_-]?key\s*=\s*['"][^'"]+['"]/gi, severity: 'high', description: 'Hardcoded API key' },
|
|
|
|
|
{ pattern: /secret\s*=\s*['"][^'"]+['"]/gi, severity: 'high', description: 'Hardcoded secret' },
|
|
|
|
|
{ pattern: /token\s*=\s*['"][^'"]+['"]/gi, severity: 'medium', description: 'Hardcoded token' }
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
return vulnerabilities;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-20 17:53:46 +08:00
|
|
|
private async scanConfigurationVulnerabilities(): Promise<Array<{ severity: string; description: string; location?: string }>> {
|
|
|
|
|
const vulnerabilities: Array<{ severity: string; description: string; location?: string }> = [];
|
2026-03-19 01:39:34 +08:00
|
|
|
|
|
|
|
|
const weakConfigs = [
|
|
|
|
|
{ check: 'NODE_ENV === "development"', severity: 'medium', description: 'Development mode in production' },
|
|
|
|
|
{ check: 'JWT_SECRET === "your-secret-key"', severity: 'critical', description: 'Default JWT secret' },
|
|
|
|
|
{ check: 'SESSION_SECRET === "change-me-in-production"', severity: 'critical', description: 'Default session secret' }
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
return vulnerabilities;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async checkDependencies(): Promise<void> {
|
|
|
|
|
this.logger.log('📦 Checking dependencies...');
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const outdated = require('child_process').execSync('npm outdated --json', {
|
|
|
|
|
encoding: 'utf8'
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (outdated) {
|
|
|
|
|
const outdatedPackages = JSON.parse(outdated);
|
|
|
|
|
await this.redisService.set(
|
|
|
|
|
'security:dependencies:outdated',
|
|
|
|
|
JSON.stringify(outdatedPackages),
|
|
|
|
|
'EX',
|
|
|
|
|
86400
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
this.logger.warn(`⚠️ ${Object.keys(outdatedPackages).length} outdated packages found`);
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
this.logger.log('✅ All dependencies are up to date');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async analyzeCodeSecurity(): Promise<void> {
|
|
|
|
|
this.logger.log('🔍 Analyzing code security...');
|
|
|
|
|
|
|
|
|
|
const securityIssues = {
|
|
|
|
|
sqlInjection: 0,
|
|
|
|
|
xss: 0,
|
|
|
|
|
hardcodedSecrets: 0,
|
|
|
|
|
weakEncryption: 0
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
await this.redisService.set(
|
|
|
|
|
'security:code:analysis',
|
|
|
|
|
JSON.stringify(securityIssues),
|
|
|
|
|
'EX',
|
|
|
|
|
86400
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
this.logger.log('✅ Code security analysis completed');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async logSecurityEvent(event: Partial<SecurityAuditLog>): Promise<void> {
|
|
|
|
|
const auditLog: SecurityAuditLog = {
|
|
|
|
|
id: `audit-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
|
|
|
timestamp: new Date().toISOString(),
|
|
|
|
|
action: event.action || 'unknown',
|
|
|
|
|
resource: event.resource || 'unknown',
|
|
|
|
|
details: event.details || {},
|
|
|
|
|
severity: event.severity || 'low',
|
|
|
|
|
status: event.status || 'success',
|
|
|
|
|
userId: event.userId,
|
|
|
|
|
tenantId: event.tenantId,
|
|
|
|
|
ipAddress: event.ipAddress,
|
|
|
|
|
userAgent: event.userAgent
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
this.auditLogs.push(auditLog);
|
|
|
|
|
|
|
|
|
|
if (this.auditLogs.length > this.maxAuditLogs) {
|
|
|
|
|
this.auditLogs.shift();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await this.cacheAuditLog(auditLog);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async cacheAuditLog(auditLog: SecurityAuditLog): Promise<void> {
|
|
|
|
|
try {
|
|
|
|
|
await this.redisService.set(
|
|
|
|
|
`security:audit:${auditLog.id}`,
|
|
|
|
|
JSON.stringify(auditLog),
|
|
|
|
|
'EX',
|
|
|
|
|
2592000 // 30 days
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
await this.redisService.lpush(
|
|
|
|
|
'security:audit:recent',
|
|
|
|
|
JSON.stringify(auditLog)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
await this.redisService.ltrim('security:audit:recent', 0, 999);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
this.logger.warn('Failed to cache audit log', error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async getSecurityMetrics(): Promise<SecurityMetrics> {
|
|
|
|
|
try {
|
|
|
|
|
const metrics = await this.redisService.get('security:metrics:latest');
|
|
|
|
|
return metrics ? JSON.parse(metrics) : await this.analyzeSecurityMetrics();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
return await this.analyzeSecurityMetrics();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async getSecurityAlerts(): Promise<SecurityAlert[]> {
|
|
|
|
|
try {
|
|
|
|
|
const alerts = await this.redisService.lrange('security:alerts:latest', 0, 99);
|
2026-03-20 17:53:46 +08:00
|
|
|
return alerts.map((alert: string) => JSON.parse(alert));
|
2026-03-19 01:39:34 +08:00
|
|
|
} catch (error) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async getAuditLogs(limit: number = 100): Promise<SecurityAuditLog[]> {
|
|
|
|
|
try {
|
|
|
|
|
const logs = await this.redisService.lrange('security:audit:recent', 0, limit - 1);
|
2026-03-20 17:53:46 +08:00
|
|
|
return logs.map((log: string) => JSON.parse(log));
|
2026-03-19 01:39:34 +08:00
|
|
|
} catch (error) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async generateSecurityReport(): Promise<any> {
|
|
|
|
|
const metrics = await this.getSecurityMetrics();
|
|
|
|
|
const alerts = await this.getSecurityAlerts();
|
|
|
|
|
const auditLogs = await this.getAuditLogs(50);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
timestamp: new Date().toISOString(),
|
|
|
|
|
metrics,
|
|
|
|
|
alerts: {
|
|
|
|
|
total: alerts.length,
|
|
|
|
|
open: alerts.filter(a => a.status === 'open').length,
|
|
|
|
|
investigating: alerts.filter(a => a.status === 'investigating').length,
|
|
|
|
|
resolved: alerts.filter(a => a.status === 'resolved').length,
|
|
|
|
|
recent: alerts.slice(0, 10)
|
|
|
|
|
},
|
|
|
|
|
auditLogs: {
|
|
|
|
|
total: auditLogs.length,
|
|
|
|
|
recent: auditLogs.slice(0, 20)
|
|
|
|
|
},
|
|
|
|
|
compliance: {
|
|
|
|
|
score: metrics.complianceScore,
|
|
|
|
|
status: metrics.complianceScore >= 90 ? 'compliant' : metrics.complianceScore >= 70 ? 'partial' : 'non-compliant'
|
|
|
|
|
},
|
|
|
|
|
recommendations: [
|
|
|
|
|
'Review and address open security alerts',
|
|
|
|
|
'Keep dependencies up to date',
|
|
|
|
|
'Regular security audits',
|
|
|
|
|
'Implement security training',
|
|
|
|
|
'Monitor security metrics continuously'
|
|
|
|
|
]
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async shutdown(): Promise<void> {
|
|
|
|
|
this.logger.log('🛑 Shutting down Security Hardening Service...');
|
|
|
|
|
|
|
|
|
|
if (this.securityCheckInterval) {
|
|
|
|
|
clearInterval(this.securityCheckInterval);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await this.redisService.del('security:checks:latest');
|
|
|
|
|
await this.redisService.del('security:metrics:latest');
|
|
|
|
|
await this.redisService.del('security:alerts:latest');
|
|
|
|
|
|
|
|
|
|
this.logger.log('✅ Security Hardening Service shutdown completed');
|
|
|
|
|
}
|
|
|
|
|
}
|