trading-platform/docs/02-definicion-modulos/OQI-004-investment-accounts/especificaciones/ET-INV-007-security.md
rckrdmrd a7cca885f0 feat: Major platform documentation and architecture updates
Changes include:
- Updated architecture documentation
- Enhanced module definitions (OQI-001 to OQI-008)
- ML integration documentation updates
- Trading strategies documentation
- Orchestration and inventory updates
- Docker configuration updates

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 05:33:35 -06:00

25 KiB

id title type status priority epic project version created_date updated_date
ET-INV-007 Seguridad y Validaciones Technical Specification Done Alta OQI-004 trading-platform 1.0.0 2025-12-05 2026-01-04

ET-INV-007: Seguridad y Validaciones

Epic: OQI-004 Cuentas de Inversión Versión: 1.0 Fecha: 2025-12-05 Responsable: Requirements-Analyst


1. Descripción

Define las medidas de seguridad, validaciones y controles para el módulo de cuentas de inversión:

  • Autenticación y autorización
  • Validación de datos de entrada
  • KYC (Know Your Customer) básico
  • Límites de transacciones
  • Prevención de fraude
  • Auditoría y logging
  • Encriptación de datos sensibles

2. Arquitectura de Seguridad

┌─────────────────────────────────────────────────────────────────┐
│                    Security Architecture                         │
├─────────────────────────────────────────────────────────────────┤
│                                                                   │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │                  Request Layer                           │   │
│  │  - Rate Limiting                                         │   │
│  │  - CORS                                                  │   │
│  │  - Helmet (Security Headers)                            │   │
│  └──────────────────────────────────────────────────────────┘   │
│                              │                                   │
│                              ▼                                   │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │              Authentication Layer                        │   │
│  │  - JWT Validation                                        │   │
│  │  - Token Refresh                                         │   │
│  │  - Session Management                                    │   │
│  └──────────────────────────────────────────────────────────┘   │
│                              │                                   │
│                              ▼                                   │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │              Authorization Layer                         │   │
│  │  - Role-Based Access Control (RBAC)                     │   │
│  │  - Resource Ownership Verification                      │   │
│  │  - Action Permissions                                   │   │
│  └──────────────────────────────────────────────────────────┘   │
│                              │                                   │
│                              ▼                                   │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │              Validation Layer                            │   │
│  │  - Input Sanitization                                   │   │
│  │  - Business Rules Validation                            │   │
│  │  - Amount Limits                                        │   │
│  └──────────────────────────────────────────────────────────┘   │
│                              │                                   │
│                              ▼                                   │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │              Fraud Detection                             │   │
│  │  - Suspicious Activity Detection                        │   │
│  │  - Velocity Checks                                      │   │
│  │  - Anomaly Detection                                    │   │
│  └──────────────────────────────────────────────────────────┘   │
│                              │                                   │
│                              ▼                                   │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │              Audit & Logging                             │   │
│  │  - Activity Logs                                        │   │
│  │  - Security Events                                      │   │
│  │  - Compliance Reporting                                 │   │
│  └──────────────────────────────────────────────────────────┘   │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

3. Autenticación y Autorización

3.1 Auth Middleware

// src/middlewares/auth.middleware.ts

import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
import { AppError } from '../utils/errors';
import { logger } from '../utils/logger';

interface JwtPayload {
  user_id: string;
  email: string;
  role: string;
  exp: number;
}

// Extender Request type
declare global {
  namespace Express {
    interface Request {
      user?: {
        id: string;
        email: string;
        role: string;
      };
    }
  }
}

/**
 * Middleware de autenticación JWT
 */
export const authenticate = async (
  req: Request,
  res: Response,
  next: NextFunction
): Promise<void> => {
  try {
    const authHeader = req.headers.authorization;

    if (!authHeader || !authHeader.startsWith('Bearer ')) {
      throw new AppError('No authentication token provided', 401);
    }

    const token = authHeader.substring(7);
    const secret = process.env.JWT_SECRET!;

    // Verificar token
    const decoded = jwt.verify(token, secret) as JwtPayload;

    // Adjuntar usuario al request
    req.user = {
      id: decoded.user_id,
      email: decoded.email,
      role: decoded.role,
    };

    // Log de acceso
    logger.debug('User authenticated', {
      user_id: req.user.id,
      endpoint: req.path,
      method: req.method,
    });

    next();
  } catch (error: any) {
    if (error.name === 'TokenExpiredError') {
      logger.warn('Expired token', { path: req.path });
      return next(new AppError('Token expired', 401));
    }

    if (error.name === 'JsonWebTokenError') {
      logger.warn('Invalid token', { path: req.path });
      return next(new AppError('Invalid token', 401));
    }

    next(error);
  }
};

/**
 * Middleware para requerir rol de admin
 */
export const requireAdmin = (
  req: Request,
  res: Response,
  next: NextFunction
): void => {
  if (!req.user) {
    throw new AppError('Authentication required', 401);
  }

  if (req.user.role !== 'admin') {
    logger.warn('Unauthorized admin access attempt', {
      user_id: req.user.id,
      role: req.user.role,
      path: req.path,
    });
    throw new AppError('Admin access required', 403);
  }

  next();
};

/**
 * Middleware para verificar ownership de recurso
 */
export const requireOwnership = (resourceType: 'account' | 'withdrawal') => {
  return async (
    req: Request,
    res: Response,
    next: NextFunction
  ): Promise<void> => {
    try {
      if (!req.user) {
        throw new AppError('Authentication required', 401);
      }

      const resourceId = req.params.id;

      // Verificar ownership según tipo de recurso
      // Esto se debe implementar en el repository
      const repository = new InvestmentRepository();
      let isOwner = false;

      if (resourceType === 'account') {
        const account = await repository.getAccountById(resourceId);
        isOwner = account?.user_id === req.user.id;
      } else if (resourceType === 'withdrawal') {
        const withdrawal = await repository.getWithdrawalRequestById(resourceId);
        isOwner = withdrawal?.user_id === req.user.id;
      }

      if (!isOwner && req.user.role !== 'admin') {
        logger.warn('Unauthorized resource access attempt', {
          user_id: req.user.id,
          resource_type: resourceType,
          resource_id: resourceId,
        });
        throw new AppError('Access denied', 403);
      }

      next();
    } catch (error) {
      next(error);
    }
  };
};

4. Validaciones de Negocio

4.1 Investment Validator Service

// src/services/validation/investment-validator.service.ts

import { InvestmentRepository } from '../../modules/investment/investment.repository';
import { AppError } from '../../utils/errors';
import { logger } from '../../utils/logger';

export class InvestmentValidatorService {
  private repository: InvestmentRepository;

  constructor() {
    this.repository = new InvestmentRepository();
  }

  /**
   * Valida que el usuario pueda crear una cuenta
   */
  async validateAccountCreation(userId: string, productId: string): Promise<void> {
    // Verificar KYC completado
    const kycStatus = await this.checkKYCStatus(userId);
    if (!kycStatus.completed) {
      throw new AppError('KYC verification required before investing', 400);
    }

    // Verificar límite de cuentas por usuario
    const accountCount = await this.repository.getAccountCountByUser(userId);
    const MAX_ACCOUNTS = parseInt(process.env.MAX_ACCOUNTS_PER_USER || '10');

    if (accountCount >= MAX_ACCOUNTS) {
      throw new AppError(`Maximum ${MAX_ACCOUNTS} accounts allowed`, 400);
    }

    // Verificar que no exista cuenta duplicada
    const existingAccount = await this.repository.getAccountByUserAndProduct(
      userId,
      productId
    );

    if (existingAccount) {
      throw new AppError('Account already exists for this product', 409);
    }
  }

  /**
   * Valida un depósito
   */
  async validateDeposit(
    userId: string,
    accountId: string,
    amount: number
  ): Promise<void> {
    // Verificar cuenta existe y pertenece al usuario
    const account = await this.repository.getAccountById(accountId);

    if (!account) {
      throw new AppError('Account not found', 404);
    }

    if (account.user_id !== userId) {
      throw new AppError('Access denied', 403);
    }

    if (account.status !== 'active') {
      throw new AppError('Account is not active', 409);
    }

    // Verificar monto mínimo
    const MIN_DEPOSIT = parseFloat(process.env.MIN_DEPOSIT_AMOUNT || '50');
    if (amount < MIN_DEPOSIT) {
      throw new AppError(`Minimum deposit is $${MIN_DEPOSIT}`, 400);
    }

    // Verificar monto máximo diario
    const dailyTotal = await this.repository.getDailyDepositTotal(userId);
    const MAX_DAILY = parseFloat(process.env.MAX_DAILY_DEPOSIT || '50000');

    if (dailyTotal + amount > MAX_DAILY) {
      throw new AppError(`Daily deposit limit of $${MAX_DAILY} exceeded`, 400);
    }

    // Verificar límite del producto
    const product = await this.repository.getProductById(account.product_id);

    if (product.max_investment) {
      const totalInvested = account.total_deposited + amount;
      if (totalInvested > product.max_investment) {
        throw new AppError(
          `Maximum investment for this product is $${product.max_investment}`,
          400
        );
      }
    }

    // Velocity check - máximo 5 depósitos por hora
    const recentDeposits = await this.repository.getRecentDeposits(userId, 3600);
    if (recentDeposits.length >= 5) {
      throw new AppError('Too many deposits. Please try again later', 429);
    }
  }

  /**
   * Valida un retiro
   */
  async validateWithdrawal(
    userId: string,
    accountId: string,
    amount: number
  ): Promise<void> {
    // Verificar cuenta
    const account = await this.repository.getAccountById(accountId);

    if (!account) {
      throw new AppError('Account not found', 404);
    }

    if (account.user_id !== userId) {
      throw new AppError('Access denied', 403);
    }

    if (account.status !== 'active') {
      throw new AppError('Account is not active', 409);
    }

    // Verificar balance suficiente
    if (amount > account.current_balance) {
      throw new AppError('Insufficient balance', 400);
    }

    // Verificar monto mínimo de retiro
    const MIN_WITHDRAWAL = parseFloat(process.env.MIN_WITHDRAWAL_AMOUNT || '50');
    if (amount < MIN_WITHDRAWAL) {
      throw new AppError(`Minimum withdrawal is $${MIN_WITHDRAWAL}`, 400);
    }

    // Verificar que no haya solicitud de retiro pendiente
    const pendingWithdrawal = await this.repository.getPendingWithdrawalByAccount(
      accountId
    );

    if (pendingWithdrawal) {
      throw new AppError('There is already a pending withdrawal request', 409);
    }

    // Verificar límite diario de retiros
    const dailyWithdrawals = await this.repository.getDailyWithdrawalTotal(userId);
    const MAX_DAILY_WITHDRAWAL = parseFloat(
      process.env.MAX_DAILY_WITHDRAWAL || '25000'
    );

    if (dailyWithdrawals + amount > MAX_DAILY_WITHDRAWAL) {
      throw new AppError(
        `Daily withdrawal limit of $${MAX_DAILY_WITHDRAWAL} exceeded`,
        400
      );
    }

    // Verificar lock period (ej: no retiros en primeros 30 días)
    const LOCK_PERIOD_DAYS = parseInt(process.env.ACCOUNT_LOCK_PERIOD_DAYS || '0');
    if (LOCK_PERIOD_DAYS > 0) {
      const accountAge = Date.now() - new Date(account.opened_at).getTime();
      const lockPeriodMs = LOCK_PERIOD_DAYS * 24 * 60 * 60 * 1000;

      if (accountAge < lockPeriodMs) {
        const daysRemaining = Math.ceil(
          (lockPeriodMs - accountAge) / (24 * 60 * 60 * 1000)
        );
        throw new AppError(
          `Account is locked for withdrawals. ${daysRemaining} days remaining`,
          400
        );
      }
    }
  }

  /**
   * Verifica estado de KYC del usuario
   */
  private async checkKYCStatus(userId: string): Promise<{
    completed: boolean;
    level: string;
  }> {
    // Implementar integración con servicio de KYC
    // Por ahora, retornar mock
    return {
      completed: true,
      level: 'basic',
    };
  }

  /**
   * Detecta actividad sospechosa
   */
  async detectSuspiciousActivity(userId: string): Promise<boolean> {
    // Múltiples cuentas creadas en corto tiempo
    const recentAccounts = await this.repository.getRecentAccountsByUser(
      userId,
      86400
    ); // 24h

    if (recentAccounts.length >= 3) {
      logger.warn('Suspicious: Multiple accounts created', { user_id: userId });
      return true;
    }

    // Patrón de depósito-retiro rápido
    const recentTransactions = await this.repository.getRecentTransactions(
      userId,
      3600
    ); // 1h

    const hasDepositAndWithdrawal =
      recentTransactions.some((t) => t.type === 'deposit') &&
      recentTransactions.some((t) => t.type === 'withdrawal');

    if (hasDepositAndWithdrawal) {
      logger.warn('Suspicious: Quick deposit-withdrawal pattern', {
        user_id: userId,
      });
      return true;
    }

    return false;
  }
}

5. Rate Limiting

5.1 Rate Limiter Middleware

// src/middlewares/rate-limit.middleware.ts

import rateLimit from 'express-rate-limit';
import RedisStore from 'rate-limit-redis';
import { createClient } from 'redis';

// Cliente Redis para rate limiting distribuido
const redisClient = createClient({
  url: process.env.REDIS_URL || 'redis://localhost:6379',
});

redisClient.connect();

/**
 * Rate limiter general para API
 */
export const apiLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutos
  max: 100, // 100 requests por ventana
  message: 'Too many requests, please try again later',
  standardHeaders: true,
  legacyHeaders: false,
  store: new RedisStore({
    client: redisClient,
    prefix: 'rl:api:',
  }),
});

/**
 * Rate limiter estricto para operaciones sensibles
 */
export const strictLimiter = rateLimit({
  windowMs: 60 * 60 * 1000, // 1 hora
  max: 5, // 5 requests por hora
  message: 'Too many requests for this operation',
  store: new RedisStore({
    client: redisClient,
    prefix: 'rl:strict:',
  }),
});

/**
 * Rate limiter personalizado
 */
export const customRateLimit = (maxRequests: number, windowSeconds: number) => {
  return rateLimit({
    windowMs: windowSeconds * 1000,
    max: maxRequests,
    store: new RedisStore({
      client: redisClient,
      prefix: 'rl:custom:',
    }),
  });
};

6. Encriptación de Datos

6.1 Encryption Service

// src/services/security/encryption.service.ts

import crypto from 'crypto';

export class EncryptionService {
  private algorithm = 'aes-256-gcm';
  private key: Buffer;

  constructor() {
    const secret = process.env.ENCRYPTION_KEY!;
    this.key = crypto.scryptSync(secret, 'salt', 32);
  }

  /**
   * Encripta datos sensibles
   */
  encrypt(text: string): string {
    const iv = crypto.randomBytes(16);
    const cipher = crypto.createCipheriv(this.algorithm, this.key, iv);

    let encrypted = cipher.update(text, 'utf8', 'hex');
    encrypted += cipher.final('hex');

    const authTag = cipher.getAuthTag();

    // Retornar iv:authTag:encrypted
    return `${iv.toString('hex')}:${authTag.toString('hex')}:${encrypted}`;
  }

  /**
   * Desencripta datos
   */
  decrypt(encryptedData: string): string {
    const parts = encryptedData.split(':');
    const iv = Buffer.from(parts[0], 'hex');
    const authTag = Buffer.from(parts[1], 'hex');
    const encrypted = parts[2];

    const decipher = crypto.createDecipheriv(this.algorithm, this.key, iv);
    decipher.setAuthTag(authTag);

    let decrypted = decipher.update(encrypted, 'hex', 'utf8');
    decrypted += decipher.final('utf8');

    return decrypted;
  }

  /**
   * Hash de datos (one-way)
   */
  hash(data: string): string {
    return crypto.createHash('sha256').update(data).digest('hex');
  }
}

6.2 Uso en Modelo de Datos

// Encriptar datos de banco antes de guardar
const encryptionService = new EncryptionService();

const destinationDetails = {
  bank_account: '****1234',
  routing_number: encryptionService.encrypt('026009593'),
  account_holder_name: 'John Doe',
};

await repository.createWithdrawalRequest({
  // ...
  destination_details: destinationDetails,
});

7. Auditoría y Logging

7.1 Audit Logger

// src/services/security/audit-logger.service.ts

import { logger } from '../../utils/logger';

export enum AuditAction {
  ACCOUNT_CREATED = 'ACCOUNT_CREATED',
  DEPOSIT_INITIATED = 'DEPOSIT_INITIATED',
  DEPOSIT_COMPLETED = 'DEPOSIT_COMPLETED',
  WITHDRAWAL_REQUESTED = 'WITHDRAWAL_REQUESTED',
  WITHDRAWAL_APPROVED = 'WITHDRAWAL_APPROVED',
  WITHDRAWAL_REJECTED = 'WITHDRAWAL_REJECTED',
  WITHDRAWAL_COMPLETED = 'WITHDRAWAL_COMPLETED',
  ACCOUNT_PAUSED = 'ACCOUNT_PAUSED',
  ACCOUNT_CLOSED = 'ACCOUNT_CLOSED',
  SUSPICIOUS_ACTIVITY = 'SUSPICIOUS_ACTIVITY',
}

interface AuditLogEntry {
  action: AuditAction;
  user_id: string;
  resource_type: string;
  resource_id: string;
  details?: Record<string, any>;
  ip_address?: string;
  user_agent?: string;
}

export class AuditLoggerService {
  /**
   * Registra evento de auditoría
   */
  log(entry: AuditLogEntry): void {
    logger.info('AUDIT', {
      timestamp: new Date().toISOString(),
      action: entry.action,
      user_id: entry.user_id,
      resource_type: entry.resource_type,
      resource_id: entry.resource_id,
      details: entry.details,
      ip_address: entry.ip_address,
      user_agent: entry.user_agent,
    });

    // Opcionalmente, guardar en tabla de auditoría
    // await auditRepository.create(entry);
  }

  /**
   * Log de evento de seguridad
   */
  logSecurityEvent(
    event: string,
    userId: string,
    severity: 'low' | 'medium' | 'high' | 'critical',
    details?: Record<string, any>
  ): void {
    logger.warn('SECURITY_EVENT', {
      timestamp: new Date().toISOString(),
      event,
      user_id: userId,
      severity,
      details,
    });

    // Si es crítico, enviar alerta
    if (severity === 'critical') {
      // await alertService.sendSecurityAlert(event, userId, details);
    }
  }
}

7.2 Uso en Controllers

// En InvestmentController
const auditLogger = new AuditLoggerService();

async createAccount(req: Request, res: Response) {
  // ... lógica de creación ...

  auditLogger.log({
    action: AuditAction.ACCOUNT_CREATED,
    user_id: req.user!.id,
    resource_type: 'account',
    resource_id: account.id,
    details: {
      product_id: data.product_id,
      initial_investment: data.initial_investment,
    },
    ip_address: req.ip,
    user_agent: req.headers['user-agent'],
  });

  // ... respuesta ...
}

8. Configuración de Seguridad

8.1 Variables de Entorno

# Authentication
JWT_SECRET=your-super-secret-jwt-key-change-in-production
JWT_EXPIRATION=1d
JWT_REFRESH_EXPIRATION=7d

# Encryption
ENCRYPTION_KEY=your-super-secret-encryption-key-32-chars

# Rate Limiting
REDIS_URL=redis://localhost:6379
RATE_LIMIT_WINDOW_MS=900000
RATE_LIMIT_MAX_REQUESTS=100

# Business Limits
MAX_ACCOUNTS_PER_USER=10
MIN_DEPOSIT_AMOUNT=50.00
MAX_DAILY_DEPOSIT=50000.00
MIN_WITHDRAWAL_AMOUNT=50.00
MAX_DAILY_WITHDRAWAL=25000.00
ACCOUNT_LOCK_PERIOD_DAYS=30

# KYC
REQUIRE_KYC_FOR_INVESTMENT=true
KYC_SERVICE_URL=https://kyc-service.example.com

8.2 Helmet Configuration

// src/app.ts

import helmet from 'helmet';

app.use(
  helmet({
    contentSecurityPolicy: {
      directives: {
        defaultSrc: ["'self'"],
        styleSrc: ["'self'", "'unsafe-inline'"],
        scriptSrc: ["'self'"],
        imgSrc: ["'self'", 'data:', 'https:'],
      },
    },
    hsts: {
      maxAge: 31536000,
      includeSubDomains: true,
      preload: true,
    },
  })
);

9. Testing de Seguridad

9.1 Security Tests

// tests/security/auth.test.ts

describe('Authentication Security', () => {
  it('should reject requests without token', async () => {
    const response = await request(app).get('/api/v1/investment/accounts');

    expect(response.status).toBe(401);
  });

  it('should reject expired tokens', async () => {
    const expiredToken = generateExpiredToken();

    const response = await request(app)
      .get('/api/v1/investment/accounts')
      .set('Authorization', `Bearer ${expiredToken}`);

    expect(response.status).toBe(401);
    expect(response.body.error).toContain('expired');
  });

  it('should reject access to other users accounts', async () => {
    const user1Token = await getAuthToken('user1');
    const user2AccountId = 'account-belongs-to-user2';

    const response = await request(app)
      .get(`/api/v1/investment/accounts/${user2AccountId}`)
      .set('Authorization', `Bearer ${user1Token}`);

    expect(response.status).toBe(403);
  });
});

10. Checklist de Seguridad

10.1 Pre-Deployment Security Checklist

  • Todas las rutas requieren autenticación
  • JWT secret es fuerte y único
  • Encryption key es de 32 caracteres
  • Rate limiting configurado en todos los endpoints
  • CORS configurado correctamente
  • Helmet habilitado con CSP
  • Logs de auditoría funcionando
  • Validaciones de input en todos los endpoints
  • Ownership verificado en recursos sensibles
  • KYC habilitado para nuevas cuentas
  • Límites de transacciones configurados
  • Datos de banco encriptados
  • HTTPS forzado en producción
  • Variables de entorno seguras
  • Secrets no en código fuente

11. Referencias

  • OWASP Top 10
  • JWT Best Practices
  • PCI DSS Compliance
  • GDPR Data Protection
  • Express.js Security Best Practices