Enable Discovery debug logging for production troubleshooting

- Add DISCOVERY_LOG_LEVEL=debug
- Add DISCOVERY_SHOW_PROGRESS=true
- Temporary changes for debugging InitializerProcessor fixes on production
This commit is contained in:
2025-08-11 20:13:26 +02:00
parent 59fd3dd3b1
commit 55a330b223
3683 changed files with 2956207 additions and 16948 deletions

View File

@@ -0,0 +1,904 @@
# 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
```php
// ✅ 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
```php
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
```php
// ✅ 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
```php
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
```php
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
```php
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
```php
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
```php
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
```php
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
```php
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
```php
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
```php
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
```php
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
```php
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);
}
}