- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
31 KiB
31 KiB
Auth Module Security Guidelines
Sicherheitsrichtlinien und Best Practices für das Auth Module des Custom PHP Frameworks.
Security Architecture
Defense in Depth Strategy
Das Auth Module implementiert mehrschichtige Sicherheitsmaßnahmen:
┌─────────────────────────────────────────────────────────────┐
│ Application Layer │
├─────────────────────────────────────────────────────────────┤
│ • Rate Limiting & Account Lockout │
│ • Session Security & IP Tracking │
│ • Multi-Factor Authentication │
│ • Password Strength Validation │
├─────────────────────────────────────────────────────────────┤
│ Framework Layer │
├─────────────────────────────────────────────────────────────┤
│ • Secure Password Hashing (Argon2ID) │
│ • Timing-Safe Comparisons │
│ • Cryptographically Secure Random Generation │
│ • Hash Value Object with Algorithm Validation │
├─────────────────────────────────────────────────────────────┤
│ Infrastructure Layer │
├─────────────────────────────────────────────────────────────┤
│ • HTTPS Enforcement │
│ • Secure Headers (HSTS, CSP) │
│ • Database Security & Prepared Statements │
│ • Logging & Monitoring │
└─────────────────────────────────────────────────────────────┘
Password Security
Secure Password Hashing
// ✅ Recommended: Argon2ID with high memory cost
$passwordHasher = new PasswordHasher(
kdf: $keyDerivationFunction,
defaultAlgorithm: 'argon2id',
defaultSecurityLevel: PasswordHasher::LEVEL_HIGH // Production
);
// Security parameters for different levels:
// LEVEL_HIGH: 128 MB memory, 6 iterations, 4 threads
// LEVEL_STANDARD: 64 MB memory, 4 iterations, 3 threads
// LEVEL_LOW: 32 MB memory, 2 iterations, 2 threads
// ❌ Avoid: Weak algorithms or low security levels in production
$weakHasher = new PasswordHasher(
kdf: $kdf,
defaultAlgorithm: 'pbkdf2-sha256', // Less secure than Argon2ID
defaultSecurityLevel: PasswordHasher::LEVEL_LOW
);
Password Policy Enforcement
final readonly class PasswordPolicy
{
public const int MIN_LENGTH = 12; // Increased from 8
public const int MIN_COMPLEXITY_TYPES = 3; // Upper, lower, numbers, special
public const int MIN_STRENGTH_SCORE = 70; // High security threshold
public function validatePassword(string $password): PasswordPolicyResult
{
$violations = [];
// Length requirement
if (mb_strlen($password) < self::MIN_LENGTH) {
$violations[] = "Password must be at least " . self::MIN_LENGTH . " characters";
}
// Complexity requirements
$complexityScore = $this->calculateComplexity($password);
if ($complexityScore < self::MIN_COMPLEXITY_TYPES) {
$violations[] = "Password must include at least " . self::MIN_COMPLEXITY_TYPES . " character types";
}
// Entropy check
if ($this->calculateEntropy($password) < 50) {
$violations[] = "Password lacks sufficient entropy";
}
// Dictionary check
if ($this->isCommonPassword($password)) {
$violations[] = "Password is too common";
}
// Personal information check
if ($this->containsPersonalInfo($password)) {
$violations[] = "Password should not contain personal information";
}
return new PasswordPolicyResult(
isValid: empty($violations),
violations: $violations,
strengthScore: $this->calculateStrengthScore($password)
);
}
private function calculateEntropy(string $password): float
{
$length = strlen($password);
$charsetSize = 0;
if (preg_match('/[a-z]/', $password)) $charsetSize += 26;
if (preg_match('/[A-Z]/', $password)) $charsetSize += 26;
if (preg_match('/[0-9]/', $password)) $charsetSize += 10;
if (preg_match('/[^A-Za-z0-9]/', $password)) $charsetSize += 32;
return $length * log($charsetSize, 2);
}
private function isCommonPassword(string $password): bool
{
// Check against common password lists (e.g., Have I Been Pwned)
$commonPasswords = [
'password', '123456', 'password123', 'admin', 'qwerty',
'letmein', 'welcome', 'monkey', 'dragon', '123456789'
];
return in_array(strtolower($password), $commonPasswords, true);
}
}
Automatic Password Rehashing
// ✅ Implement automatic rehashing on authentication
public function authenticate(string $identifier, string $password): AuthenticationResult
{
$user = $this->repository->findByIdentifier($identifier);
if (!$user || !$this->passwordHasher->verify($password, $user->getHashedPassword())) {
return AuthenticationResult::failed('Invalid credentials');
}
// Critical: Check for rehashing need
if ($this->passwordHasher->needsRehash($user->getHashedPassword())) {
$newHash = $this->passwordHasher->hash($password);
$this->repository->updateUserPassword($user->getId(), $newHash);
$this->logger->info('Password automatically rehashed', [
'user_id' => $user->getId(),
'old_algorithm' => $user->getHashedPassword()->getAlgorithm(),
'new_algorithm' => $newHash->getAlgorithm()
]);
}
return AuthenticationResult::success($user);
}
Session Security
Secure Session Management
final readonly class SecureSessionManager
{
public function __construct(
private AuthenticationService $authService,
private SessionSecurity $sessionSecurity
) {}
public function createSecureSession(
User $user,
IpAddress $ipAddress,
UserAgent $userAgent
): AuthenticationSession {
// Generate cryptographically secure session ID
$sessionId = $this->generateSecureSessionId();
// Create session with security attributes
$session = new AuthenticationSession(
id: $sessionId,
userId: $user->getId(),
ipAddress: $ipAddress,
userAgent: $userAgent,
createdAt: new \DateTimeImmutable(),
expiresAt: new \DateTimeImmutable('+1 hour'),
lastActivity: new \DateTimeImmutable(),
securityAttributes: new SessionSecurityAttributes(
isSecure: true,
isHttpOnly: true,
sameSite: 'Strict',
fingerprint: $this->generateFingerprint($ipAddress, $userAgent)
)
);
// Set secure session cookie
$this->setSecureSessionCookie($sessionId, $session->getExpiresAt());
return $session;
}
private function generateSecureSessionId(): SessionId
{
// Use framework's SessionIdGenerator for consistency
return $this->sessionIdGenerator->generate();
}
private function setSecureSessionCookie(SessionId $sessionId, \DateTimeImmutable $expiresAt): void
{
setcookie('session_id', $sessionId->toString(), [
'expires' => $expiresAt->getTimestamp(),
'path' => '/',
'domain' => '', // Let browser determine
'secure' => true, // HTTPS only
'httponly' => true, // No JavaScript access
'samesite' => 'Strict' // CSRF protection
]);
}
private function generateFingerprint(IpAddress $ipAddress, UserAgent $userAgent): string
{
return Hash::sha256(
$ipAddress->toString() . '|' .
$userAgent->toString() . '|' .
$_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? ''
)->toString();
}
}
Session Hijacking Protection
final readonly class SessionHijackingProtection
{
public function validateSession(
SessionId $sessionId,
IpAddress $currentIp,
UserAgent $currentUserAgent
): SessionValidationResult {
$session = $this->repository->findSessionById($sessionId);
if (!$session) {
return SessionValidationResult::invalid('Session not found');
}
// Check session expiration
if ($session->isExpired()) {
$this->repository->deleteSession($sessionId);
return SessionValidationResult::expired();
}
// IP address consistency check (configurable)
if ($this->config->checkIpConsistency) {
if (!$session->getIpAddress()->equals($currentIp)) {
$this->logSecurityEvent('session_ip_mismatch', [
'session_id' => $sessionId->toString(),
'original_ip' => (string) $session->getIpAddress(),
'current_ip' => (string) $currentIp
]);
// Suspicious activity - invalidate session
$this->repository->deleteSession($sessionId);
return SessionValidationResult::suspicious('IP address mismatch');
}
}
// User Agent consistency (less strict)
if (!$this->isUserAgentConsistent($session->getUserAgent(), $currentUserAgent)) {
$this->logSecurityEvent('session_user_agent_change', [
'session_id' => $sessionId->toString(),
'original_ua' => (string) $session->getUserAgent(),
'current_ua' => (string) $currentUserAgent
]);
}
// Update session activity
$this->repository->updateSessionActivity($sessionId, $currentIp);
return SessionValidationResult::valid($session);
}
private function isUserAgentConsistent(UserAgent $original, UserAgent $current): bool
{
// Allow minor version changes but detect major browser changes
$originalBrowser = $this->extractBrowser($original);
$currentBrowser = $this->extractBrowser($current);
return $originalBrowser === $currentBrowser;
}
}
Token Security
Remember Token Security with Hash Value Object
final readonly class RememberTokenSecurity
{
public function createRememberToken(string $userId): RememberToken
{
// Generate cryptographically secure token
$tokenValue = bin2hex(random_bytes(32)); // 256-bit token
// Hash token using Hash Value Object for type safety
$tokenHash = Hash::sha256($tokenValue);
$rememberToken = new RememberToken(
hash: $tokenHash,
userId: $userId,
createdAt: new \DateTimeImmutable(),
expiresAt: new \DateTimeImmutable('+30 days'),
lastUsed: null
);
$this->repository->storeRememberToken($rememberToken);
// Return token with plain text value (only shown once)
return $rememberToken->withPlainTextValue($tokenValue);
}
public function validateRememberToken(string $tokenValue): RememberTokenValidation
{
// Hash provided token for comparison
$tokenHash = Hash::sha256($tokenValue);
$rememberToken = $this->repository->findRememberToken($tokenHash);
if (!$rememberToken) {
$this->logSecurityEvent('invalid_remember_token', [
'token_hash' => $tokenHash->toShort(8) // Only log first 8 chars
]);
return RememberTokenValidation::invalid();
}
// Check expiration
if ($rememberToken->isExpired()) {
$this->repository->deleteRememberToken($tokenHash);
return RememberTokenValidation::expired();
}
// Update last used timestamp
$this->repository->updateRememberTokenUsage($tokenHash);
return RememberTokenValidation::valid($rememberToken);
}
public function rotateRememberToken(Hash $oldTokenHash): RememberToken
{
$oldToken = $this->repository->findRememberToken($oldTokenHash);
if (!$oldToken) {
throw new SecurityException('Cannot rotate non-existent token');
}
// Create new token
$newToken = $this->createRememberToken($oldToken->getUserId());
// Delete old token
$this->repository->deleteRememberToken($oldTokenHash);
$this->logSecurityEvent('remember_token_rotated', [
'user_id' => $oldToken->getUserId(),
'old_token_created' => $oldToken->getCreatedAt()->format('Y-m-d H:i:s')
]);
return $newToken;
}
}
// Benefits of using Hash Value Object:
// ✅ Type safety - tokens can't be confused with other strings
// ✅ Algorithm specification - explicit SHA-256 usage
// ✅ Timing-safe comparison - uses hash_equals() internally
// ✅ Validation - ensures proper hash format and length
// ✅ Framework consistency - follows value object patterns
Rate Limiting & Brute Force Protection
Advanced Rate Limiting
final readonly class AdvancedRateLimiter implements RateLimitService
{
public function __construct(
private Cache $cache,
private Logger $logger,
private RateLimitConfig $config
) {}
public function checkRateLimit(
IpAddress $ipAddress,
string $action,
string $identifier = null
): RateLimitResult {
// Multiple rate limiting strategies
$checks = [
$this->checkIpBasedLimit($ipAddress, $action),
$this->checkIdentifierBasedLimit($identifier, $action),
$this->checkGlobalLimit($action),
$this->checkAdaptiveLimit($ipAddress, $action)
];
foreach ($checks as $check) {
if ($check->isLimited()) {
$this->logRateLimitViolation($ipAddress, $action, $check);
return $check;
}
}
// Record successful attempt
$this->recordAttempt($ipAddress, $action, $identifier);
return RateLimitResult::allowed();
}
private function checkAdaptiveLimit(IpAddress $ipAddress, string $action): RateLimitResult
{
$suspiciousScore = $this->calculateSuspiciousScore($ipAddress);
// Adaptive limits based on suspicious activity
$maxAttempts = match (true) {
$suspiciousScore > 80 => 1, // Highly suspicious
$suspiciousScore > 60 => 2, // Suspicious
$suspiciousScore > 40 => 3, // Moderately suspicious
default => 5 // Normal
};
$key = "adaptive_limit:{$action}:" . $ipAddress->toString();
$attempts = $this->cache->get($key, 0);
if ($attempts >= $maxAttempts) {
return RateLimitResult::limited(
reason: 'Adaptive rate limit exceeded',
retryAfter: 900,
metadata: ['suspicious_score' => $suspiciousScore]
);
}
return RateLimitResult::allowed();
}
private function calculateSuspiciousScore(IpAddress $ipAddress): int
{
$score = 0;
// Check for various suspicious indicators
if ($this->isFromTorNetwork($ipAddress)) $score += 30;
if ($this->isFromVpnProvider($ipAddress)) $score += 15;
if ($this->hasRecentFailures($ipAddress)) $score += 20;
if ($this->isFromUncommonGeolocation($ipAddress)) $score += 10;
if ($this->hasRapidRequests($ipAddress)) $score += 25;
return min(100, $score);
}
private function logRateLimitViolation(
IpAddress $ipAddress,
string $action,
RateLimitResult $result
): void {
$this->logger->warning('Rate limit violation', [
'ip_address' => (string) $ipAddress,
'action' => $action,
'reason' => $result->getReason(),
'retry_after' => $result->getRetryAfter(),
'metadata' => $result->getMetadata()
]);
}
}
Account Lockout Protection
final readonly class AccountLockoutService
{
public function __construct(
private UserRepository $repository,
private Logger $logger,
private NotificationService $notifications
) {}
public function handleFailedLogin(string $identifier, IpAddress $ipAddress): void
{
$user = $this->repository->findByIdentifier($identifier);
if (!$user) {
return; // Don't reveal if user exists
}
$attempts = $this->repository->incrementFailedAttempts($user->getId());
// Progressive lockout strategy
$lockoutDuration = $this->calculateLockoutDuration($attempts);
if ($attempts >= 3) {
$this->repository->lockAccount($user->getId(), $lockoutDuration);
$this->logger->warning('Account locked due to failed attempts', [
'user_id' => $user->getId(),
'attempts' => $attempts,
'lockout_duration' => $lockoutDuration,
'ip_address' => (string) $ipAddress
]);
// Notify user of lockout (rate limited to prevent abuse)
$this->sendLockoutNotification($user, $attempts, $lockoutDuration);
}
}
private function calculateLockoutDuration(int $attempts): int
{
// Exponential backoff with jitter
return match (true) {
$attempts >= 10 => 3600 + random_int(0, 1800), // 1-2.5 hours
$attempts >= 7 => 1800 + random_int(0, 900), // 30-45 minutes
$attempts >= 5 => 900 + random_int(0, 300), // 15-20 minutes
$attempts >= 3 => 300 + random_int(0, 120), // 5-7 minutes
default => 0
};
}
private function sendLockoutNotification(
User $user,
int $attempts,
int $lockoutDuration
): void {
// Rate limit notifications to prevent spam
$notificationKey = "lockout_notification:" . $user->getId();
if ($this->cache->has($notificationKey)) {
return;
}
$this->cache->put($notificationKey, true, 300); // 5 minutes
$this->notifications->sendSecurityAlert($user->getEmail(), [
'type' => 'account_lockout',
'attempts' => $attempts,
'lockout_duration_minutes' => ceil($lockoutDuration / 60),
'timestamp' => new \DateTimeImmutable()
]);
}
}
Multi-Factor Authentication Security
TOTP Implementation with Security Features
final readonly class SecureTotpService
{
private const int SECRET_LENGTH = 32; // 160-bit secret
private const int WINDOW_SIZE = 1; // Allow 1 time step tolerance
private const int TIME_STEP = 30; // 30-second time steps
public function generateSecret(): string
{
return Base32::encode(random_bytes(self::SECRET_LENGTH));
}
public function verify(string $code, string $secret, ?int $timestamp = null): bool
{
$timestamp = $timestamp ?? time();
$timeStep = intval($timestamp / self::TIME_STEP);
// Check current and adjacent time windows to account for clock drift
for ($i = -self::WINDOW_SIZE; $i <= self::WINDOW_SIZE; $i++) {
$calculatedCode = $this->calculateTotp($secret, $timeStep + $i);
if (hash_equals($code, $calculatedCode)) {
// Prevent replay attacks by storing used codes
$this->markCodeAsUsed($secret, $timeStep + $i);
return true;
}
}
return false;
}
private function calculateTotp(string $secret, int $timeStep): string
{
$binarySecret = Base32::decode($secret);
$timeBytes = pack('N*', 0) . pack('N*', $timeStep);
$hash = hash_hmac('sha1', $timeBytes, $binarySecret, true);
$offset = ord($hash[19]) & 0xf;
$code = (
((ord($hash[$offset]) & 0x7f) << 24) |
((ord($hash[$offset + 1]) & 0xff) << 16) |
((ord($hash[$offset + 2]) & 0xff) << 8) |
(ord($hash[$offset + 3]) & 0xff)
) % 1000000;
return str_pad((string) $code, 6, '0', STR_PAD_LEFT);
}
private function markCodeAsUsed(string $secret, int $timeStep): void
{
// Prevent replay attacks
$key = 'totp_used:' . Hash::sha256($secret . ':' . $timeStep)->toShort(16);
$this->cache->put($key, true, self::TIME_STEP * 2);
}
}
Backup Code Security
final readonly class BackupCodeService
{
private const int CODE_LENGTH = 8;
private const int CODE_COUNT = 8;
public function generateBackupCodes(string $userId): array
{
$codes = [];
for ($i = 0; $i < self::CODE_COUNT; $i++) {
$code = $this->generateHumanReadableCode();
$codes[] = $code;
// Store hashed version
$backupCode = new MfaBackupCode(
userId: $userId,
codeHash: Hash::sha256($code),
createdAt: new \DateTimeImmutable(),
usedAt: null
);
$this->repository->storeBackupCode($backupCode);
}
return $codes;
}
public function verifyBackupCode(string $userId, string $code): bool
{
$codeHash = Hash::sha256($code);
$backupCode = $this->repository->findBackupCode($userId, $codeHash);
if (!$backupCode || $backupCode->isUsed()) {
return false;
}
// Mark as used (one-time use only)
$this->repository->markBackupCodeAsUsed($backupCode, new \DateTimeImmutable());
return true;
}
private function generateHumanReadableCode(): string
{
// Generate readable codes avoiding confusing characters
$chars = '23456789ABCDEFGHJKLMNPQRSTUVWXYZ'; // No 0, 1, I, O
$code = '';
for ($i = 0; $i < self::CODE_LENGTH; $i++) {
$code .= $chars[random_int(0, strlen($chars) - 1)];
}
// Format as XXXX-XXXX for readability
return substr($code, 0, 4) . '-' . substr($code, 4, 4);
}
}
Monitoring & Incident Response
Security Event Monitoring
final readonly class SecurityEventMonitor
{
public function __construct(
private Logger $securityLogger,
private AlertingService $alerting,
private MetricsCollector $metrics
) {}
public function recordSecurityEvent(SecurityEvent $event): void
{
// Log all security events
$this->securityLogger->warning('Security event recorded', [
'event_type' => $event->getType(),
'severity' => $event->getSeverity(),
'user_id' => $event->getUserId(),
'ip_address' => (string) $event->getIpAddress(),
'user_agent' => (string) $event->getUserAgent(),
'metadata' => $event->getMetadata(),
'timestamp' => $event->getTimestamp()->format('Y-m-d H:i:s')
]);
// Update security metrics
$this->metrics->increment('auth.security_events', [
'type' => $event->getType(),
'severity' => $event->getSeverity()
]);
// Send alerts for critical events
if ($event->isCritical()) {
$this->alerting->sendSecurityAlert($event);
}
// Check for attack patterns
$this->analyzeForAttackPatterns($event);
}
private function analyzeForAttackPatterns(SecurityEvent $event): void
{
$ipAddress = $event->getIpAddress();
$recentEvents = $this->getRecentSecurityEvents($ipAddress, minutes: 10);
// Detect brute force attacks
$failedLogins = array_filter($recentEvents,
fn($e) => $e->getType() === 'authentication_failed'
);
if (count($failedLogins) >= 5) {
$this->alerting->sendAlert(new BruteForceDetectionAlert(
ipAddress: $ipAddress,
attemptCount: count($failedLogins),
timeWindow: 10
));
}
// Detect credential stuffing
$uniqueIdentifiers = array_unique(array_map(
fn($e) => $e->getMetadata()['identifier'] ?? null,
$failedLogins
));
if (count($uniqueIdentifiers) >= 10) {
$this->alerting->sendAlert(new CredentialStuffingAlert(
ipAddress: $ipAddress,
uniqueIdentifiers: count($uniqueIdentifiers)
));
}
}
}
Automated Response System
final readonly class AutomatedSecurityResponse
{
public function handleSecurityEvent(SecurityEvent $event): void
{
match ($event->getType()) {
'brute_force_detected' => $this->handleBruteForce($event),
'credential_stuffing_detected' => $this->handleCredentialStuffing($event),
'session_hijacking_suspected' => $this->handleSessionHijacking($event),
'suspicious_login_pattern' => $this->handleSuspiciousLogin($event),
default => null
};
}
private function handleBruteForce(SecurityEvent $event): void
{
$ipAddress = $event->getIpAddress();
// Immediately block IP for 1 hour
$this->firewall->blockIpAddress($ipAddress, duration: 3600);
// Increase monitoring for this IP
$this->monitoring->increaseWatchLevel($ipAddress);
// Notify security team
$this->alerting->sendUrgentAlert(new BruteForceBlockAlert($ipAddress));
}
private function handleSessionHijacking(SecurityEvent $event): void
{
$userId = $event->getUserId();
$sessionId = $event->getMetadata()['session_id'];
// Immediately terminate all user sessions
$this->authService->logoutAll($userId);
// Require password reset
$this->userService->requirePasswordReset($userId);
// Send security notification to user
$user = $this->userRepository->findById($userId);
$this->notifications->sendSecurityBreach($user->getEmail(), [
'incident_type' => 'session_hijacking',
'affected_session' => $sessionId,
'timestamp' => $event->getTimestamp()
]);
}
}
Compliance & Audit
GDPR Compliance
final readonly class GdprCompliantAuthService
{
public function processPersonalData(User $user, string $purpose): void
{
// Log data processing for GDPR audit trail
$this->auditLogger->info('Personal data processed', [
'user_id' => $user->getId(),
'data_types' => ['email', 'login_timestamps', 'ip_addresses'],
'processing_purpose' => $purpose,
'legal_basis' => 'legitimate_interest', // or 'consent'
'retention_period' => '2_years'
]);
}
public function exportUserData(string $userId): UserDataExport
{
$user = $this->repository->findById($userId);
if (!$user) {
throw new UserNotFoundException($userId);
}
return new UserDataExport([
'personal_data' => [
'user_id' => $user->getId(),
'email' => (string) $user->getEmail(),
'created_at' => $user->getCreatedAt()->format('c'),
'last_login' => $user->getLastLoginAt()?->format('c')
],
'authentication_data' => [
'password_last_changed' => $user->getPasswordChangedAt()?->format('c'),
'mfa_enabled' => $user->hasMfaEnabled(),
'failed_login_attempts' => $this->repository->getFailedLoginAttempts($userId)
],
'session_data' => $this->getSessionHistory($userId),
'security_events' => $this->getSecurityEventHistory($userId)
]);
}
public function deleteUserData(string $userId): void
{
// GDPR right to erasure (right to be forgotten)
$this->repository->anonymizeUser($userId);
$this->repository->deleteAllUserSessions($userId);
$this->repository->deleteAllUserRememberTokens($userId);
$this->repository->deleteUserSecurityEvents($userId);
$this->auditLogger->info('User data deleted per GDPR request', [
'user_id' => $userId,
'deletion_timestamp' => (new \DateTimeImmutable())->format('c')
]);
}
}
Security Audit Logging
final readonly class SecurityAuditLogger
{
public function logAuthenticationEvent(string $eventType, array $context): void
{
$auditRecord = [
'event_type' => $eventType,
'timestamp' => (new \DateTimeImmutable())->format('c'),
'user_id' => $context['user_id'] ?? null,
'session_id' => $context['session_id'] ?? null,
'ip_address' => $context['ip_address'] ?? null,
'user_agent' => $context['user_agent'] ?? null,
'success' => $context['success'] ?? false,
'failure_reason' => $context['failure_reason'] ?? null,
'risk_score' => $this->calculateRiskScore($context)
];
// Store in secure audit log (append-only, tamper-evident)
$this->auditStorage->append($auditRecord);
// Forward to SIEM if configured
if ($this->siemForwarder) {
$this->siemForwarder->forward($auditRecord);
}
}
private function calculateRiskScore(array $context): int
{
$score = 0;
// Location-based risk
if (isset($context['ip_address'])) {
$location = $this->geolocator->locate($context['ip_address']);
if ($location->isHighRiskCountry()) $score += 25;
if ($location->isFromTor()) $score += 50;
}
// Time-based risk
$hour = (int) date('H');
if ($hour < 6 || $hour > 22) $score += 10; // Outside business hours
// Failure patterns
if (!($context['success'] ?? true)) $score += 20;
return min(100, $score);
}
}