chore: complete update

This commit is contained in:
2025-07-17 16:24:20 +02:00
parent 899227b0a4
commit 64a7051137
1300 changed files with 85570 additions and 2756 deletions

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace App\Application\Security\Events\Access;
use App\Application\Security\SecurityEvent;
use App\Application\Security\SecurityEventType;
final class CsrfViolation implements SecurityEvent
{
public function __construct(
public readonly string $requestPath,
public readonly string $method,
) {}
public SecurityEventType $type {
get {
return SecurityEventType::CSRF_VIOLATION;
}
}
}

View File

@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace App\Application\Security\Events\Auth;
use App\Application\Security\ValueObjects\{OWASPEventIdentifier, OWASPLogLevel};
final class AccountLockedEvent
{
public function __construct(
public readonly string $email,
public readonly string $reason,
public readonly int $failedAttempts
) {}
public function getOWASPEventIdentifier(): OWASPEventIdentifier
{
return OWASPEventIdentifier::accountLocked($this->maskEmail($this->email));
}
public function getOWASPLogLevel(): OWASPLogLevel
{
return OWASPLogLevel::ERROR;
}
public function getDescription(): string
{
return "Account {$this->maskEmail($this->email)} locked";
}
public function getEventData(): array
{
return [
'username' => $this->maskEmail($this->email),
'lock_reason' => $this->reason,
'failed_attempts' => $this->failedAttempts
];
}
private function maskEmail(string $email): string
{
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
return $email;
}
[$local, $domain] = explode('@', $email, 2);
$maskedLocal = substr($local, 0, 2) . str_repeat('*', max(0, strlen($local) - 2));
return $maskedLocal . '@' . $domain;
}
}

View File

@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace App\Application\Security\Events\Auth;
use App\Application\Security\{OWASPSecurityEvent};
use App\Application\Security\ValueObjects\{OWASPEventIdentifier, OWASPLogLevel, MaskedEmail};
final class AuthenticationFailedEvent implements OWASPSecurityEvent
{
private MaskedEmail $maskedEmail;
public function __construct(
public readonly string $email,
public readonly ?string $reason = null,
public readonly int $failedAttempts = 1
) {
$this->maskedEmail = MaskedEmail::fromString($this->email);
}
public function getOWASPEventIdentifier(): OWASPEventIdentifier
{
return OWASPEventIdentifier::authenticationFailure($this->maskedEmail->toString());
}
public function getOWASPLogLevel(): OWASPLogLevel
{
return OWASPLogLevel::WARN;
}
public function getDescription(): string
{
return "User {$this->maskedEmail->toString()} login failed" .
($this->reason ? " - {$this->reason}" : '');
}
public function getEventData(): array
{
return [
'email' => $this->maskedEmail->toString(),
'reason' => $this->reason,
'failed_attempts' => $this->failedAttempts,
'failure_reason' => $this->reason ?? 'invalid_credentials'
];
}
public function getMaskedEmail(): MaskedEmail
{
return $this->maskedEmail;
}
}

View File

@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace App\Application\Security\Events\Auth;
use App\Application\Security\{OWASPSecurityEvent};
use App\Application\Security\ValueObjects\{OWASPEventIdentifier, OWASPLogLevel, MaskedEmail};
final class AuthenticationSuccessEvent implements OWASPSecurityEvent
{
private MaskedEmail $maskedEmail;
public function __construct(
public readonly string $email,
public readonly string $sessionId,
public readonly ?string $method = 'password'
) {
$this->maskedEmail = MaskedEmail::fromString($this->email);
}
public function getOWASPEventIdentifier(): OWASPEventIdentifier
{
return OWASPEventIdentifier::authenticationSuccess($this->maskedEmail->toString());
}
public function getOWASPLogLevel(): OWASPLogLevel
{
return OWASPLogLevel::INFO;
}
public function getDescription(): string
{
return "User {$this->maskedEmail->toString()} login successfully";
}
public function getEventData(): array
{
return [
'username' => $this->maskedEmail->toString(),
'session_id' => hash('sha256', $this->sessionId), // Session-ID hashen für Sicherheit
'method' => $this->method
];
}
public function getMaskedEmail(): MaskedEmail
{
return $this->maskedEmail;
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace App\Application\Security\Events\Auth;
use App\Application\Security\SecurityEvent;
use App\Application\Security\SecurityEventType;
final class LoginFailed implements SecurityEvent
{
public function __construct(
public string $email
) {}
public SecurityEventType $type {
get {
return SecurityEventType::LOGIN_FAILED;
}
}
}

View File

@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace App\Application\Security\Events\Auth;
use App\Application\Security\ValueObjects\{OWASPEventIdentifier, OWASPLogLevel};
final class PasswordChangedEvent
{
public function __construct(
public readonly string $email,
public readonly string $method = 'self_service'
) {}
public function getOWASPEventIdentifier(): OWASPEventIdentifier
{
return OWASPEventIdentifier::passwordChange($this->maskEmail($this->email));
}
public function getOWASPLogLevel(): OWASPLogLevel
{
return OWASPLogLevel::INFO;
}
public function getDescription(): string
{
return "User {$this->maskEmail($this->email)} changed password";
}
public function getEventData(): array
{
return [
'username' => $this->maskEmail($this->email),
'change_method' => $this->method
];
}
private function maskEmail(string $email): string
{
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
return $email;
}
[$local, $domain] = explode('@', $email, 2);
$maskedLocal = substr($local, 0, 2) . str_repeat('*', max(0, strlen($local) - 2));
return $maskedLocal . '@' . $domain;
}
}

View File

@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace App\Application\Security\Events\Auth;
use App\Application\Security\{OWASPSecurityEvent};
use App\Application\Security\ValueObjects\{OWASPEventIdentifier, OWASPLogLevel, MaskedEmail};
final class SessionTerminatedEvent implements OWASPSecurityEvent
{
private MaskedEmail $maskedEmail;
public function __construct(
public readonly string $email,
public readonly string $sessionId,
public readonly string $reason = 'logout'
) {
$this->maskedEmail = MaskedEmail::fromString($this->email);
}
public function getOWASPEventIdentifier(): OWASPEventIdentifier
{
return OWASPEventIdentifier::sessionTermination($this->maskedEmail->toString());
}
public function getOWASPLogLevel(): OWASPLogLevel
{
return OWASPLogLevel::INFO;
}
public function getDescription(): string
{
return "User {$this->maskedEmail->toString()} logged out";
}
public function getEventData(): array
{
return [
'username' => $this->maskedEmail->toString(),
'session_id' => hash('sha256', $this->sessionId),
'termination_reason' => $this->reason
];
}
public function getMaskedEmail(): MaskedEmail
{
return $this->maskedEmail;
}
}

View File

@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace App\Application\Security\Events\Authorization;
use App\Application\Security\ValueObjects\{OWASPEventIdentifier, OWASPLogLevel};
final class AccessDeniedEvent
{
public function __construct(
public readonly string $email,
public readonly string $resource,
public readonly string $action,
public readonly ?string $requiredPermission = null
) {}
public function getOWASPEventIdentifier(): OWASPEventIdentifier
{
return OWASPEventIdentifier::authorizationFailure(
$this->maskEmail($this->email),
$this->resource
);
}
public function getOWASPLogLevel(): OWASPLogLevel
{
return OWASPLogLevel::WARN;
}
public function getDescription(): string
{
return "Access denied for user {$this->maskEmail($this->email)} to resource {$this->resource}";
}
public function getEventData(): array
{
return [
'username' => $this->maskEmail($this->email),
'resource' => $this->resource,
'action' => $this->action,
'required_permission' => $this->requiredPermission
];
}
private function maskEmail(string $email): string
{
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
return $email;
}
[$local, $domain] = explode('@', $email, 2);
$maskedLocal = substr($local, 0, 2) . str_repeat('*', max(0, strlen($local) - 2));
return $maskedLocal . '@' . $domain;
}
}

View File

@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace App\Application\Security\Events\Authorization;
use App\Application\Security\{OWASPSecurityEvent};
use App\Application\Security\ValueObjects\{OWASPEventIdentifier, OWASPLogLevel, MaskedEmail};
final class PrivilegeEscalationEvent implements OWASPSecurityEvent
{
private MaskedEmail $maskedEmail;
public function __construct(
public readonly string $email,
public readonly string $fromRole,
public readonly string $toRole,
public readonly string $method,
public readonly bool $successful = false
) {
$this->maskedEmail = MaskedEmail::fromString($this->email);
}
public function getOWASPEventIdentifier(): OWASPEventIdentifier
{
return OWASPEventIdentifier::privilegeEscalation(
$this->maskedEmail->toString(),
$this->fromRole,
$this->toRole
);
}
public function getOWASPLogLevel(): OWASPLogLevel
{
return OWASPLogLevel::FATAL;
}
public function getDescription(): string
{
$status = $this->successful ? 'successful' : 'attempted';
return "Privilege escalation {$status} by user {$this->maskedEmail->toString()}";
}
public function getEventData(): array
{
return [
'username' => $this->maskedEmail->toString(),
'from_role' => $this->fromRole,
'to_role' => $this->toRole,
'escalation_method' => $this->method,
'successful' => $this->successful
];
}
public function getMaskedEmail(): MaskedEmail
{
return $this->maskedEmail;
}
}

View File

@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace App\Application\Security\Events\Crypto;
use App\Application\Security\{OWASPSecurityEvent};
use App\Application\Security\ValueObjects\{OWASPEventIdentifier, OWASPLogLevel, MaskedEmail};
final class CryptographicFailureEvent implements OWASPSecurityEvent
{
private ?MaskedEmail $maskedEmail;
public function __construct(
public readonly string $operation,
public readonly string $algorithm,
public readonly string $errorMessage,
public readonly ?string $email = null
) {
$this->maskedEmail = $this->email ? MaskedEmail::fromString($this->email) : null;
}
public function getOWASPEventIdentifier(): OWASPEventIdentifier
{
return OWASPEventIdentifier::cryptographicFailure($this->operation);
}
public function getOWASPLogLevel(): OWASPLogLevel
{
return OWASPLogLevel::ERROR;
}
public function getDescription(): string
{
return "Cryptographic failure in {$this->operation}";
}
public function getEventData(): array
{
return [
'operation' => $this->operation,
'algorithm' => $this->algorithm,
'error_message' => $this->errorMessage,
'username' => $this->maskedEmail?->toString() ?? 'system'
];
}
public function getMaskedEmail(): ?MaskedEmail
{
return $this->maskedEmail;
}
}

View File

@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace App\Application\Security\Events\File;
use App\Application\Security\{OWASPSecurityEvent};
use App\Application\Security\ValueObjects\{OWASPEventIdentifier, OWASPLogLevel, MaskedEmail};
final class SuspiciousFileUploadEvent implements OWASPSecurityEvent
{
private ?MaskedEmail $maskedEmail;
public function __construct(
public readonly string $filename,
public readonly string $mimeType,
public readonly int $fileSize,
public readonly string $suspicionReason,
public readonly ?string $email = null
) {
$this->maskedEmail = $this->email ? MaskedEmail::fromString($this->email) : null;
}
public function getOWASPEventIdentifier(): OWASPEventIdentifier
{
return OWASPEventIdentifier::fileUploadFailure($this->filename);
}
public function getOWASPLogLevel(): OWASPLogLevel
{
return OWASPLogLevel::ERROR;
}
public function getDescription(): string
{
return "Suspicious file upload: {$this->filename}";
}
public function getEventData(): array
{
return [
'filename' => $this->sanitizeFilename($this->filename),
'mime_type' => $this->mimeType,
'file_size' => $this->fileSize,
'suspicion_reason' => $this->suspicionReason,
'username' => $this->maskedEmail?->toString() ?? 'anonymous'
];
}
public function getMaskedEmail(): ?MaskedEmail
{
return $this->maskedEmail;
}
private function sanitizeFilename(string $filename): string
{
return preg_replace('/[^\w\.\-]/', '_', $filename);
}
}

View File

@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace App\Application\Security\Events\Input;
use App\Application\Security\{OWASPSecurityEvent};
use App\Application\Security\ValueObjects\{OWASPEventIdentifier, OWASPLogLevel, MaskedEmail};
final class InputValidationFailureEvent implements OWASPSecurityEvent
{
private ?MaskedEmail $maskedEmail;
public function __construct(
public readonly string $fieldName,
public readonly string $invalidValue,
public readonly string $validationRule,
public readonly ?string $email = null
) {
$this->maskedEmail = $this->email ? MaskedEmail::fromString($this->email) : null;
}
public function getOWASPEventIdentifier(): OWASPEventIdentifier
{
return OWASPEventIdentifier::inputValidationFailure($this->fieldName);
}
public function getOWASPLogLevel(): OWASPLogLevel
{
return OWASPLogLevel::WARN;
}
public function getDescription(): string
{
return "Input validation failure for field {$this->fieldName}";
}
public function getEventData(): array
{
return [
'field_name' => $this->fieldName,
'invalid_value' => $this->sanitizeForLog($this->invalidValue),
'validation_rule' => $this->validationRule,
'username' => $this->maskedEmail?->toString() ?? 'anonymous'
];
}
public function getMaskedEmail(): ?MaskedEmail
{
return $this->maskedEmail;
}
private function sanitizeForLog(string $value): string
{
// Maximal 100 Zeichen und gefährliche Zeichen entfernen
$sanitized = substr($value, 0, 100);
return preg_replace('/[^\w\s\.\-@]/', '***', $sanitized);
}
}

View File

@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace App\Application\Security\Events\Input;
use App\Application\Security\{OWASPSecurityEvent};
use App\Application\Security\ValueObjects\{OWASPEventIdentifier, OWASPLogLevel, MaskedEmail};
final class MaliciousInputDetectedEvent implements OWASPSecurityEvent
{
private ?MaskedEmail $maskedEmail;
public function __construct(
public readonly string $fieldName,
public readonly string $attackPattern,
public readonly string $sanitizedValue,
public readonly ?string $email = null
) {
$this->maskedEmail = $this->email ? MaskedEmail::fromString($this->email) : null;
}
public function getOWASPEventIdentifier(): OWASPEventIdentifier
{
return OWASPEventIdentifier::maliciousInput($this->attackPattern);
}
public function getOWASPLogLevel(): OWASPLogLevel
{
return OWASPLogLevel::ERROR;
}
public function getDescription(): string
{
return "Malicious input detected: {$this->attackPattern}";
}
public function getEventData(): array
{
return [
'field_name' => $this->fieldName,
'attack_pattern' => $this->attackPattern,
'sanitized_value' => $this->sanitizedValue,
'username' => $this->maskedEmail?->toString() ?? 'anonymous'
];
}
public function getMaskedEmail(): ?MaskedEmail
{
return $this->maskedEmail;
}
}

View File

@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace App\Application\Security\Events\Input;
use App\Application\Security\{OWASPSecurityEvent};
use App\Application\Security\ValueObjects\{OWASPEventIdentifier, OWASPLogLevel, MaskedEmail};
final class SqlInjectionAttemptEvent implements OWASPSecurityEvent
{
private ?MaskedEmail $maskedEmail;
public function __construct(
public readonly string $attackPayload,
public readonly string $targetField,
public readonly string $detectionMethod,
public readonly ?string $email = null
) {
$this->maskedEmail = $this->email ? MaskedEmail::fromString($this->email) : null;
}
public function getOWASPEventIdentifier(): OWASPEventIdentifier
{
return OWASPEventIdentifier::maliciousInput('sql_injection');
}
public function getOWASPLogLevel(): OWASPLogLevel
{
return OWASPLogLevel::ERROR;
}
public function getDescription(): string
{
return "SQL injection attempt detected";
}
public function getEventData(): array
{
return [
'attack_payload' => $this->sanitizePayload($this->attackPayload),
'target_field' => $this->targetField,
'detection_method' => $this->detectionMethod,
'username' => $this->maskedEmail?->toString() ?? 'anonymous'
];
}
public function getMaskedEmail(): ?MaskedEmail
{
return $this->maskedEmail;
}
private function sanitizePayload(string $payload): string
{
// SQL-Injection-Payload nur teilweise loggen für Analyse
return substr(preg_replace('/[^\w\s\'\";=\-\(\)]/', '***', $payload), 0, 200);
}
}

View File

@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace App\Application\Security\Events\Input;
use App\Application\Security\{OWASPSecurityEvent};
use App\Application\Security\ValueObjects\{OWASPEventIdentifier, OWASPLogLevel, MaskedEmail};
final class XssAttemptEvent implements OWASPSecurityEvent
{
private ?MaskedEmail $maskedEmail;
public function __construct(
public readonly string $attackPayload,
public readonly string $targetField,
public readonly string $xssType,
public readonly ?string $email = null
) {
$this->maskedEmail = $this->email ? MaskedEmail::fromString($this->email) : null;
}
public function getOWASPEventIdentifier(): OWASPEventIdentifier
{
return OWASPEventIdentifier::maliciousInput('xss_attempt');
}
public function getOWASPLogLevel(): OWASPLogLevel
{
return OWASPLogLevel::ERROR;
}
public function getDescription(): string
{
return "XSS attempt detected: {$this->xssType}";
}
public function getEventData(): array
{
return [
'attack_payload' => $this->sanitizePayload($this->attackPayload),
'target_field' => $this->targetField,
'xss_type' => $this->xssType,
'username' => $this->maskedEmail?->toString() ?? 'anonymous'
];
}
public function getMaskedEmail(): ?MaskedEmail
{
return $this->maskedEmail;
}
private function sanitizePayload(string $payload): string
{
// HTML-Tags entfernen aber Struktur beibehalten für Analyse
return substr(htmlspecialchars($payload, ENT_QUOTES, 'UTF-8'), 0, 200);
}
}

View File

@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace App\Application\Security\Events\Network;
use App\Application\Security\{OWASPSecurityEvent};
use App\Application\Security\ValueObjects\{OWASPEventIdentifier, OWASPLogLevel, MaskedEmail};
final class SuspiciousNetworkActivityEvent implements OWASPSecurityEvent
{
private ?MaskedEmail $maskedEmail;
public function __construct(
public readonly string $sourceIp,
public readonly string $activityType,
public readonly int $requestCount,
public readonly string $timeWindow,
public readonly ?string $email = null
) {
$this->maskedEmail = $this->email ? MaskedEmail::fromString($this->email) : null;
}
public function getOWASPEventIdentifier(): OWASPEventIdentifier
{
return OWASPEventIdentifier::suspiciousNetworkActivity($this->activityType);
}
public function getOWASPLogLevel(): OWASPLogLevel
{
return OWASPLogLevel::WARN;
}
public function getDescription(): string
{
return "Suspicious network activity detected: {$this->activityType}";
}
public function getEventData(): array
{
return [
'source_ip' => $this->sourceIp,
'activity_type' => $this->activityType,
'request_count' => $this->requestCount,
'time_window' => $this->timeWindow,
'username' => $this->maskedEmail?->toString() ?? 'anonymous'
];
}
public function getMaskedEmail(): ?MaskedEmail
{
return $this->maskedEmail;
}
}

View File

@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace App\Application\Security\Events\Session;
use App\Application\Security\{OWASPSecurityEvent};
use App\Application\Security\ValueObjects\{OWASPEventIdentifier, OWASPLogLevel, MaskedEmail};
final class SessionFixationEvent implements OWASPSecurityEvent
{
private ?MaskedEmail $maskedEmail;
public function __construct(
public readonly ?string $email,
public readonly string $oldSessionId,
public readonly string $newSessionId,
public readonly string $attackVector
) {
$this->maskedEmail = $this->email ? MaskedEmail::fromString($this->email) : null;
}
public function getOWASPEventIdentifier(): OWASPEventIdentifier
{
return OWASPEventIdentifier::sessionFixation($this->maskedEmail?->toString() ?? 'anonymous');
}
public function getOWASPLogLevel(): OWASPLogLevel
{
return OWASPLogLevel::FATAL;
}
public function getDescription(): string
{
return "Session fixation attack detected for user {$this->maskedEmail?->toString() ?? 'anonymous'}";
}
public function getEventData(): array
{
return [
'username' => $this->maskedEmail?->toString() ?? 'anonymous',
'old_session_id' => hash('sha256', $this->oldSessionId),
'new_session_id' => hash('sha256', $this->newSessionId),
'attack_vector' => $this->attackVector
];
}
public function getMaskedEmail(): ?MaskedEmail
{
return $this->maskedEmail;
}
}

View File

@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace App\Application\Security\Events\Session;
use App\Application\Security\ValueObjects\{OWASPEventIdentifier, OWASPLogLevel};
final class SessionHijackingDetectedEvent
{
public function __construct(
public readonly string $email,
public readonly string $sessionId,
public readonly string $evidence,
public readonly ?string $suspiciousIp = null
) {}
public function getOWASPEventIdentifier(): OWASPEventIdentifier
{
return OWASPEventIdentifier::sessionHijacking($this->maskEmail($this->email));
}
public function getOWASPLogLevel(): OWASPLogLevel
{
return OWASPLogLevel::FATAL;
}
public function getDescription(): string
{
return "Session hijacking detected for user {$this->maskEmail($this->email)}";
}
public function getEventData(): array
{
return [
'username' => $this->maskEmail($this->email),
'session_id' => hash('sha256', $this->sessionId),
'evidence' => $this->evidence,
'suspicious_ip' => $this->suspiciousIp
];
}
private function maskEmail(string $email): string
{
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
return $email;
}
[$local, $domain] = explode('@', $email, 2);
$maskedLocal = substr($local, 0, 2) . str_repeat('*', max(0, strlen($local) - 2));
return $maskedLocal . '@' . $domain;
}
}

View File

@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace App\Application\Security\Events\Session;
use App\Application\Security\{OWASPSecurityEvent};
use App\Application\Security\ValueObjects\{OWASPEventIdentifier, OWASPLogLevel, MaskedEmail};
final class SessionTimeoutEvent implements OWASPSecurityEvent
{
private ?MaskedEmail $maskedEmail;
public function __construct(
public readonly ?string $email,
public readonly string $sessionId,
public readonly int $inactivityDuration,
public readonly string $reason = 'timeout'
) {
$this->maskedEmail = $this->email ? MaskedEmail::fromString($this->email) : null;
}
public function getOWASPEventIdentifier(): OWASPEventIdentifier
{
return OWASPEventIdentifier::sessionTimeout($this->maskedEmail?->toString() ?? 'anonymous');
}
public function getOWASPLogLevel(): OWASPLogLevel
{
return OWASPLogLevel::WARN;
}
public function getDescription(): string
{
return "Session timeout for user {$this->maskedEmail?->toString() ?? 'anonymous'}";
}
public function getEventData(): array
{
return [
'username' => $this->maskedEmail?->toString() ?? 'anonymous',
'session_id' => hash('sha256', $this->sessionId),
'inactivity_duration' => $this->inactivityDuration,
'timeout_reason' => $this->reason
];
}
public function getMaskedEmail(): ?MaskedEmail
{
return $this->maskedEmail;
}
}

View File

@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace App\Application\Security\Events\System;
use App\Application\Security\{OWASPSecurityEvent};
use App\Application\Security\ValueObjects\{OWASPEventIdentifier, OWASPLogLevel, MaskedEmail};
final class SystemAnomalyEvent implements OWASPSecurityEvent
{
public function __construct(
public readonly string $anomalyType,
public readonly string $description,
public readonly array $metrics,
public readonly string $severity
) {}
public function getOWASPEventIdentifier(): OWASPEventIdentifier
{
return OWASPEventIdentifier::systemAnomaly($this->anomalyType);
}
public function getOWASPLogLevel(): OWASPLogLevel
{
return match ($this->severity) {
'low' => OWASPLogLevel::INFO,
'medium' => OWASPLogLevel::WARN,
'high' => OWASPLogLevel::ERROR,
'critical' => OWASPLogLevel::FATAL,
default => OWASPLogLevel::WARN
};
}
public function getDescription(): string
{
return "System anomaly detected: {$this->anomalyType}";
}
public function getEventData(): array
{
return [
'anomaly_type' => $this->anomalyType,
'description' => $this->description,
'metrics' => $this->metrics,
'severity' => $this->severity
];
}
}

View File

@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace App\Application\Security\Events\Web;
use App\Application\Security\{OWASPSecurityEvent};
use App\Application\Security\ValueObjects\{OWASPEventIdentifier, OWASPLogLevel, MaskedEmail};
final class CsrfViolationEvent implements OWASPSecurityEvent
{
private ?MaskedEmail $maskedEmail;
public function __construct(
public readonly string $requestPath,
public readonly string $method,
public readonly ?string $expectedToken = null,
public readonly ?string $providedToken = null,
public readonly ?string $email = null
) {
$this->maskedEmail = $this->email ? MaskedEmail::fromString($this->email) : null;
}
public function getOWASPEventIdentifier(): OWASPEventIdentifier
{
return OWASPEventIdentifier::csrfViolation($this->requestPath);
}
public function getOWASPLogLevel(): OWASPLogLevel
{
return OWASPLogLevel::ERROR;
}
public function getDescription(): string
{
return "CSRF token validation failed for {$this->method} {$this->requestPath}";
}
public function getEventData(): array
{
return [
'request_path' => $this->requestPath,
'method' => $this->method,
'expected_token_hash' => $this->expectedToken ? hash('sha256', $this->expectedToken) : null,
'provided_token_hash' => $this->providedToken ? hash('sha256', $this->providedToken) : null,
'username' => $this->maskedEmail?->toString() ?? 'anonymous'
];
}
public function getMaskedEmail(): ?MaskedEmail
{
return $this->maskedEmail;
}
}

View File

@@ -0,0 +1,142 @@
<?php
declare(strict_types=1);
namespace App\Application\Security\ExceptionHandlers;
use App\Application\Security\Events\{
Authorization\AccessDeniedEvent,
Input\InputValidationFailureEvent,
System\SystemAnomalyEvent,
Crypto\CryptographicFailureEvent
};
use App\Framework\Core\Events\EventDispatcher;
use App\Framework\Core\Exceptions\{
ValidationException,
CryptographicException
};
use Psr\Log\LoggerInterface;
use Symfony\Component\Finder\Exception\AccessDeniedException;
use Throwable;
final class SecurityExceptionHandler
{
public function __construct(
private EventDispatcher $eventDispatcher,
private LoggerInterface $logger
) {}
public function handle(Throwable $exception): void
{
match (true) {
$exception instanceof AccessDeniedException => $this->handleAccessDenied($exception),
$exception instanceof ValidationException => $this->handleValidationError($exception),
$exception instanceof CryptographicException => $this->handleCryptographicError($exception),
$exception instanceof \Error => $this->handleSystemError($exception),
default => $this->handleGenericSecurityIssue($exception)
};
}
private function handleAccessDenied(AccessDeniedException $exception): void
{
$this->eventDispatcher->dispatch(new AccessDeniedEvent(
email: $this->getCurrentUserEmail(),
resource: $exception->getResource(),
action: $exception->getAction(),
requiredPermission: $exception->getRequiredPermission()
));
}
private function handleValidationError(ValidationException $exception): void
{
foreach ($exception->getErrors() as $field => $errors) {
$this->eventDispatcher->dispatch(new InputValidationFailureEvent(
fieldName: $field,
invalidValue: $exception->getInvalidValue($field) ?? '',
validationRule: implode(', ', $errors),
email: $this->getCurrentUserEmail()
));
}
}
private function handleCryptographicError(CryptographicException $exception): void
{
$this->eventDispatcher->dispatch(new CryptographicFailureEvent(
operation: $exception->getOperation(),
algorithm: $exception->getAlgorithm(),
errorMessage: $exception->getMessage(),
email: $this->getCurrentUserEmail()
));
}
private function handleSystemError(\Error $exception): void
{
$this->eventDispatcher->dispatch(new SystemAnomalyEvent(
anomalyType: 'system_error',
description: $exception->getMessage(),
metrics: [
'error_type' => get_class($exception),
'file' => $exception->getFile(),
'line' => $exception->getLine(),
'memory_usage' => memory_get_usage(true)
],
severity: 'high'
));
}
private function handleGenericSecurityIssue(Throwable $exception): void
{
// Nur bei tatsächlich sicherheitsrelevanten Exceptions loggen
if ($this->isSecurityRelevant($exception)) {
$this->logger->warning('Security-relevant exception detected', [
'exception_class' => get_class($exception),
'message' => $exception->getMessage(),
'user_email' => $this->getCurrentUserEmail(),
'security_context' => [
'ip' => $_SERVER['REMOTE_ADDR'] ?? 'unknown',
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? null,
'request_uri' => $_SERVER['REQUEST_URI'] ?? null
]
]);
}
}
private function isSecurityRelevant(Throwable $exception): bool
{
$securityRelevantClasses = [
'AccessDeniedException',
'AuthenticationException',
'AuthorizationException',
'ValidationException',
'CryptographicException',
'FileUploadException',
'SessionException'
];
$className = get_class($exception);
foreach ($securityRelevantClasses as $securityClass) {
if (str_contains($className, $securityClass)) {
return true;
}
}
// Sicherheitsrelevante Nachrichten prüfen
$message = strtolower($exception->getMessage());
$securityKeywords = [
'access denied', 'unauthorized', 'permission', 'authentication',
'authorization', 'csrf', 'xss', 'injection', 'validation failed'
];
foreach ($securityKeywords as $keyword) {
if (str_contains($message, $keyword)) {
return true;
}
}
return false;
}
private function getCurrentUserEmail(): ?string
{
return $_SESSION['user_email'] ?? null;
}
}

View File

@@ -0,0 +1,129 @@
<?php
declare(strict_types=1);
namespace App\Application\Security\Guards;
use App\Framework\Database\Example\UserRepository;
use App\Application\Security\Events\Auth\{
AuthenticationSuccessEvent,
AuthenticationFailedEvent,
AccountLockedEvent
};
use App\Framework\Core\Events\EventDispatcher;
use App\Domain\User\{User};
final class AuthenticationGuard
{
private const MAX_FAILED_ATTEMPTS = 5;
private const LOCKOUT_DURATION = 900; // 15 Minuten
public function __construct(
private EventDispatcher $eventDispatcher,
private UserRepository $userRepository
) {}
public function authenticate(string $email, string $password): ?User
{
try {
$user = $this->userRepository->findByEmail($email);
if (!$user) {
$this->dispatchFailedAttempt($email, 'user_not_found');
return null;
}
if ($this->isAccountLocked($user)) {
$this->dispatchAccountLocked($email, 'too_many_failed_attempts', $user->failed_attempts);
return null;
}
if (!$this->verifyPassword($password, $user->password_hash)) {
$this->handleFailedAttempt($user);
return null;
}
$this->handleSuccessfulLogin($user);
return $user;
} catch (\Exception $e) {
$this->dispatchFailedAttempt($email, 'authentication_error');
throw $e;
}
}
public function logout(User $user): void
{
$sessionId = session_id();
// Session invalidieren
session_destroy();
$this->eventDispatcher->dispatch(new \App\Application\Security\Events\Auth\SessionTerminatedEvent(
email: $user->email,
sessionId: $sessionId,
reason: 'manual_logout'
));
}
private function handleSuccessfulLogin(User $user): void
{
// Failed attempts zurücksetzen
$user->failed_attempts = 0;
$user->last_login = new \DateTimeImmutable();
$this->userRepository->save($user);
// Session regenerieren
session_regenerate_id(true);
$_SESSION['user_id'] = $user->id;
$this->eventDispatcher->dispatch(new AuthenticationSuccessEvent(
email: $user->email,
sessionId: session_id(),
method: 'password'
));
}
private function handleFailedAttempt(User $user): void
{
$user->failed_attempts++;
$user->last_failed_attempt = new \DateTimeImmutable();
if ($user->failed_attempts >= self::MAX_FAILED_ATTEMPTS) {
$user->locked_until = new \DateTimeImmutable('+' . self::LOCKOUT_DURATION . ' seconds');
$this->userRepository->save($user);
$this->dispatchAccountLocked($user->email, 'max_attempts_exceeded', $user->failed_attempts);
} else {
$this->userRepository->save($user);
$this->dispatchFailedAttempt($user->email, 'invalid_password', $user->failed_attempts);
}
}
private function dispatchFailedAttempt(string $email, string $reason, int $attempts = 1): void
{
$this->eventDispatcher->dispatch(new AuthenticationFailedEvent(
email: $email,
reason: $reason,
failedAttempts: $attempts
));
}
private function dispatchAccountLocked(string $email, string $reason, int $attempts): void
{
$this->eventDispatcher->dispatch(new AccountLockedEvent(
email: $email,
reason: $reason,
failedAttempts: $attempts
));
}
private function isAccountLocked(User $user): bool
{
return $user->locked_until && $user->locked_until > new \DateTimeImmutable();
}
private function verifyPassword(string $password, string $hash): bool
{
return password_verify($password, $hash);
}
}

View File

@@ -0,0 +1,106 @@
<?php
declare(strict_types=1);
namespace App\Application\Security\Middleware;
use App\Application\Security\Events\{
Web\CsrfViolationEvent,
Network\SuspiciousNetworkActivityEvent,
Input\InputValidationFailureEvent
};
use App\Framework\Core\Events\EventDispatcher;
use Psr\Http\Message\{ServerRequestInterface, ResponseInterface};
use Psr\Http\Server\{MiddlewareInterface, RequestHandlerInterface};
final class SecurityEventMiddleware implements MiddlewareInterface
{
private array $requestCounts = [];
private const RATE_LIMIT_THRESHOLD = 100;
private const TIME_WINDOW = 300; // 5 Minuten
public function __construct(
private EventDispatcher $eventDispatcher
) {}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$this->checkRateLimit($request);
$this->validateCsrfToken($request);
$response = $handler->handle($request);
$this->analyzeResponse($request, $response);
return $response;
}
private function checkRateLimit(ServerRequestInterface $request): void
{
$clientIp = $this->getClientIp($request);
$currentTime = time();
// Rate Limiting Check
if (!isset($this->requestCounts[$clientIp])) {
$this->requestCounts[$clientIp] = [];
}
// Alte Einträge entfernen
$this->requestCounts[$clientIp] = array_filter(
$this->requestCounts[$clientIp],
fn($timestamp) => $currentTime - $timestamp < self::TIME_WINDOW
);
$this->requestCounts[$clientIp][] = $currentTime;
if (count($this->requestCounts[$clientIp]) > self::RATE_LIMIT_THRESHOLD) {
$this->eventDispatcher->dispatch(new SuspiciousNetworkActivityEvent(
sourceIp: $clientIp,
activityType: 'rate_limit_exceeded',
requestCount: count($this->requestCounts[$clientIp]),
timeWindow: self::TIME_WINDOW . 's'
));
}
}
private function validateCsrfToken(ServerRequestInterface $request): void
{
$method = $request->getMethod();
if (!in_array($method, ['POST', 'PUT', 'DELETE', 'PATCH'])) {
return;
}
$sessionToken = $_SESSION['csrf_token'] ?? null;
$requestToken = $request->getParsedBody()['csrf_token'] ??
$request->getHeaderLine('X-CSRF-Token');
if (!$sessionToken || !$requestToken || !hash_equals($sessionToken, $requestToken)) {
$this->eventDispatcher->dispatch(new CsrfViolationEvent(
requestPath: $request->getUri()->getPath(),
method: $method,
expectedToken: $sessionToken,
providedToken: $requestToken
));
}
}
private function analyzeResponse(ServerRequestInterface $request, ResponseInterface $response): void
{
// Suspicious response patterns
if ($response->getStatusCode() === 403) {
// Access denied - könnte bereits von Authorization Middleware gehandhabt werden
}
if ($response->getStatusCode() >= 500) {
// Server errors - könnte auf Angriffe hindeuten
}
}
private function getClientIp(ServerRequestInterface $request): string
{
$serverParams = $request->getServerParams();
return $serverParams['HTTP_X_FORWARDED_FOR'] ??
$serverParams['HTTP_X_REAL_IP'] ??
$serverParams['REMOTE_ADDR'] ??
'unknown';
}
}

View File

@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace App\Application\Security;
use App\Application\Security\ValueObjects\{OWASPEventIdentifier, OWASPLogLevel};
interface OWASPSecurityEvent
{
/**
* Gibt den OWASP-konformen Event-Identifier zurück
*/
public function getOWASPEventIdentifier(): OWASPEventIdentifier;
/**
* Gibt das entsprechende Log-Level zurück
*/
public function getOWASPLogLevel(): OWASPLogLevel;
/**
* Gibt eine menschenlesbare Beschreibung des Events zurück
*/
public function getDescription(): string;
/**
* Gibt strukturierte Event-Daten für das Logging zurück
*/
public function getEventData(): array;
}

View File

@@ -0,0 +1,210 @@
<?php
declare(strict_types=1);
namespace App\Application\Security;
use App\Application\Security\ValueObjects\{
SecurityContext,
RequestContext,
OWASPLogFormat,
OWASPEventIdentifier,
OWASPLogLevel
};
final class OWASPSecurityEventFactory
{
public function __construct(
private string $applicationId = 'app.security'
) {}
public function createFromSecurityEvent(
SecurityEvent $event,
SecurityContext $context,
RequestContext $requestContext
): OWASPLogFormat {
$eventIdentifier = $this->createEventIdentifier($event);
$logLevel = OWASPLogLevel::fromSecurityEventType($event->type);
$description = $this->createDescription($event, $eventIdentifier);
return OWASPLogFormat::create(
$this->applicationId,
$eventIdentifier->toString(),
$logLevel->value,
$description,
$context,
$requestContext
);
}
private function createEventIdentifier(SecurityEvent $event): OWASPEventIdentifier
{
return match ($event->type) {
SecurityEventType::LOGIN_FAILED => $this->createLoginFailedIdentifier($event),
SecurityEventType::LOGIN_SUCCESS => $this->createLoginSuccessIdentifier($event),
SecurityEventType::LOGOUT => $this->createLogoutIdentifier($event),
SecurityEventType::PASSWORD_CHANGE => $this->createPasswordChangeIdentifier($event),
SecurityEventType::ACCOUNT_LOCKED => $this->createAccountLockedIdentifier($event),
SecurityEventType::ACCESS_DENIED => $this->createAccessDeniedIdentifier($event),
SecurityEventType::PRIVILEGE_ESCALATION => $this->createPrivilegeEscalationIdentifier($event),
SecurityEventType::DATA_ACCESS => $this->createDataAccessIdentifier($event),
SecurityEventType::INJECTION_ATTEMPT => $this->createInjectionAttemptIdentifier($event),
SecurityEventType::FILE_UPLOAD => $this->createFileUploadIdentifier($event),
SecurityEventType::SESSION_HIJACK => $this->createSessionHijackIdentifier($event),
SecurityEventType::SESSION_TIMEOUT => $this->createSessionTimeoutIdentifier($event),
SecurityEventType::MALWARE_DETECTED => $this->createMalwareDetectedIdentifier($event),
SecurityEventType::AUDIT_FAILURE => $this->createAuditFailureIdentifier($event),
};
}
private function createLoginFailedIdentifier(SecurityEvent $event): OWASPEventIdentifier
{
$username = $this->extractUsername($event);
return OWASPEventIdentifier::authenticationFailure($username);
}
private function createLoginSuccessIdentifier(SecurityEvent $event): OWASPEventIdentifier
{
$username = $this->extractUsername($event);
return OWASPEventIdentifier::authenticationSuccess($username);
}
private function createLogoutIdentifier(SecurityEvent $event): OWASPEventIdentifier
{
$username = $this->extractUsername($event);
return OWASPEventIdentifier::sessionTermination($username);
}
private function createPasswordChangeIdentifier(SecurityEvent $event): OWASPEventIdentifier
{
$username = $this->extractUsername($event);
return OWASPEventIdentifier::passwordChange($username);
}
private function createAccountLockedIdentifier(SecurityEvent $event): OWASPEventIdentifier
{
$username = $this->extractUsername($event);
return OWASPEventIdentifier::accountLocked($username);
}
private function createAccessDeniedIdentifier(SecurityEvent $event): OWASPEventIdentifier
{
$username = $this->extractUsername($event);
$resource = $this->extractProperty($event, 'resource', 'unknown_resource');
return OWASPEventIdentifier::authorizationFailure($username, $resource);
}
private function createPrivilegeEscalationIdentifier(SecurityEvent $event): OWASPEventIdentifier
{
$username = $this->extractUsername($event);
$fromRole = $this->extractProperty($event, 'fromRole', 'user');
$toRole = $this->extractProperty($event, 'toRole', 'admin');
return OWASPEventIdentifier::privilegeEscalation($username, $fromRole, $toRole);
}
private function createDataAccessIdentifier(SecurityEvent $event): OWASPEventIdentifier
{
$field = $this->extractProperty($event, 'field', 'unknown');
return OWASPEventIdentifier::inputValidationFailure($field);
}
private function createInjectionAttemptIdentifier(SecurityEvent $event): OWASPEventIdentifier
{
$attackType = $this->extractProperty($event, 'attackType', 'injection');
return OWASPEventIdentifier::maliciousInput($attackType);
}
private function createFileUploadIdentifier(SecurityEvent $event): OWASPEventIdentifier
{
$filename = $this->extractProperty($event, 'filename', 'unknown.file');
return OWASPEventIdentifier::fileUploadFailure($filename);
}
private function createSessionHijackIdentifier(SecurityEvent $event): OWASPEventIdentifier
{
$username = $this->extractUsername($event);
return OWASPEventIdentifier::sessionHijacking($username);
}
private function createSessionTimeoutIdentifier(SecurityEvent $event): OWASPEventIdentifier
{
$username = $this->extractUsername($event);
return OWASPEventIdentifier::sessionTimeout($username);
}
private function createMalwareDetectedIdentifier(SecurityEvent $event): OWASPEventIdentifier
{
$malwareType = $this->extractProperty($event, 'malwareType', 'unknown');
return OWASPEventIdentifier::malwareDetected($malwareType);
}
private function createAuditFailureIdentifier(SecurityEvent $event): OWASPEventIdentifier
{
$eventType = $this->extractProperty($event, 'eventType', $event->type->value);
return OWASPEventIdentifier::auditFailure($eventType);
}
private function createDescription(SecurityEvent $event, OWASPEventIdentifier $identifier): string
{
return match ($event->type) {
SecurityEventType::LOGIN_FAILED => "User {$this->extractUsername($event)} login failed",
SecurityEventType::LOGIN_SUCCESS => "User {$this->extractUsername($event)} login successfully",
SecurityEventType::LOGOUT => "User {$this->extractUsername($event)} logged out",
SecurityEventType::PASSWORD_CHANGE => "User {$this->extractUsername($event)} changed password",
SecurityEventType::ACCOUNT_LOCKED => "Account {$this->extractUsername($event)} locked",
SecurityEventType::ACCESS_DENIED => "Access denied for user {$this->extractUsername($event)} to resource {$this->extractProperty($event, 'resource', 'unknown_resource')}",
SecurityEventType::PRIVILEGE_ESCALATION => "Privilege escalation attempt by user {$this->extractUsername($event)}",
SecurityEventType::DATA_ACCESS => "Input validation failure for field {$this->extractProperty($event, 'field', 'unknown')}",
SecurityEventType::INJECTION_ATTEMPT => "Malicious input detected: {$this->extractProperty($event, 'attackType', 'injection')}",
SecurityEventType::FILE_UPLOAD => "File upload failed: {$this->extractProperty($event, 'filename', 'unknown.file')}",
SecurityEventType::SESSION_HIJACK => "Session hijacking detected for user {$this->extractUsername($event)}",
SecurityEventType::SESSION_TIMEOUT => "Session timeout for user {$this->extractUsername($event)}",
SecurityEventType::MALWARE_DETECTED => "Malware detected: {$this->extractProperty($event, 'malwareType', 'unknown')}",
SecurityEventType::AUDIT_FAILURE => "Audit logging failure for event {$event->type->value}",
};
}
private function extractUsername(SecurityEvent $event): string
{
$usernameFields = ['username', 'email', 'user', 'userId'];
foreach ($usernameFields as $field) {
$value = $this->extractProperty($event, $field);
if ($value !== null) {
return $this->maskEmail($value);
}
}
return 'anonymous';
}
private function extractProperty(SecurityEvent $event, string $propertyName, ?string $default = null): ?string
{
try {
$reflection = new \ReflectionObject($event);
if ($reflection->hasProperty($propertyName)) {
$property = $reflection->getProperty($propertyName);
$property->setAccessible(true);
$value = $property->getValue($event);
return is_string($value) ? $value : (string)$value;
}
} catch (\Exception) {
// Property nicht gefunden oder nicht lesbar
}
return $default;
}
private function maskEmail(string $value): string
{
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
return $value;
}
[$local, $domain] = explode('@', $value, 2);
$maskedLocal = substr($local, 0, 2) . str_repeat('*', max(0, strlen($local) - 2));
return $maskedLocal . '@' . $domain;
}
}

View File

@@ -0,0 +1,89 @@
<?php
declare(strict_types=1);
namespace App\Application\Security;
use App\Application\Security\ValueObjects\{
SecurityContext,
RequestContext,
OWASPLogFormat,
OWASPLogLevel
};
use App\Framework\Core\Events\OnEvent;
use Psr\Log\LoggerInterface;
use Throwable;
final class OWASPSecurityEventLogger
{
public function __construct(
private LoggerInterface $logger,
private string $applicationId = 'app.security'
) {}
/**
* Universeller Event-Handler für alle OWASP Security Events
*/
#[OnEvent]
public function logSecurityEvent(OWASPSecurityEvent $event): void
{
try {
$securityContext = SecurityContext::fromGlobals();
$requestContext = RequestContext::fromGlobals();
$owaspLogFormat = $this->createOWASPLogFormat(
$event,
$securityContext,
$requestContext
);
// OWASP-konformes JSON-Log
$this->logger->log(
$this->mapLogLevel($event->getOWASPLogLevel()),
$event->getDescription(),
[
'owasp_format' => $owaspLogFormat->toArray(),
'event_class' => $event::class,
'event_data' => $event->getEventData()
]
);
} catch (Throwable $e) {
// Fallback logging - niemals Security-Events verlieren
$this->logger->critical('AUDIT_audit_failure:owasp_logger', [
'datetime' => date('c'),
'appid' => $this->applicationId,
'event' => 'AUDIT_audit_failure:owasp_logger',
'level' => 'FATAL',
'description' => 'OWASP security event logging failed: ' . $e->getMessage(),
'original_event_class' => $event::class,
'error_trace' => $e->getTraceAsString()
]);
}
}
private function createOWASPLogFormat(
OWASPSecurityEvent $event,
SecurityContext $securityContext,
RequestContext $requestContext
): OWASPLogFormat {
return OWASPLogFormat::create(
$this->applicationId,
$event->getOWASPEventIdentifier()->toString(),
$event->getOWASPLogLevel()->value,
$event->getDescription(),
$securityContext,
$requestContext
);
}
private function mapLogLevel(OWASPLogLevel $owaspLevel): string
{
return match ($owaspLevel) {
OWASPLogLevel::DEBUG => 'debug',
OWASPLogLevel::INFO => 'info',
OWASPLogLevel::WARN => 'warning',
OWASPLogLevel::ERROR => 'error',
OWASPLogLevel::FATAL => 'critical'
};
}
}

View File

@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace App\Application\Security;
/**
* Beispiel für Logeintrag:
* {
* 'timestamp': '2025-06-01T00:52:10Z',
* 'event': 'auth.login.failed',
* 'ip': '203.0.113.42',
* 'user_agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)',
* 'request_uri': '/login',
* 'method': 'POST',
* 'user_id': null,
* 'email': 'j***@example.com',
* 'session_id': 'abc123',
* 'referrer': 'https://example.com/login',
* 'result': 'fail',
* 'request_id': 'req-3d92fcb45e'
* }
*
*/
final readonly class SecurityContext
{
public function __construct(
public string $ip,
public ?string $userAgent,
public string $method,
public string $uri,
public \DateTimeImmutable $timestamp,
public ?string $userId = null,
public ?string $sessionId = null,
public ?string $referrer = null,
public ?string $requestId = null
) {}
public static function fromGlobals(): self
{
return new self(
ip: $_SERVER['REMOTE_ADDR'] ?? 'unknown',
userAgent: $_SERVER['HTTP_USER_AGENT'] ?? null,
method: $_SERVER['REQUEST_METHOD'] ?? 'CLI',
uri: $_SERVER['REQUEST_URI'] ?? '-',
timestamp: new \DateTimeImmutable(),
userId: $_SESSION['user_id'] ?? null,
sessionId: session_id() ?: null,
referrer: $_SERVER['HTTP_REFERER'] ?? null,
requestId: $_SERVER['HTTP_X_REQUEST_ID'] ?? bin2hex(random_bytes(8))
);
}
}

View File

@@ -0,0 +1,10 @@
<?php
namespace App\Application\Security;
interface SecurityEvent
{
public SecurityEventType $type {
get;
}
}

View File

@@ -0,0 +1,84 @@
<?php
declare(strict_types=1);
namespace App\Application\Security;
use App\Application\Security\ValueObjects\{SecurityContext as SecurityContextVO, RequestContext};
use App\Framework\Core\Events\OnEvent;
use Psr\Log\LoggerInterface;
use Throwable;
final class SecurityEventLogger
{
public function __construct(
private LoggerInterface $logger,
private ?OWASPSecurityEventFactory $eventFactory = null
) {
$this->eventFactory ??= new OWASPSecurityEventFactory();
}
#[OnEvent]
public function log(SecurityEvent $event): void
{
try {
// OWASP-konforme Logging
$this->logOWASPFormat($event);
// Fallback: Ursprüngliches Format beibehalten
$this->logLegacyFormat($event);
} catch (Throwable $e) {
// Fallback logging - niemals Security-Events verlieren
$this->logger->critical('AUDIT_audit_failure:security_logger', [
'datetime' => date('c'),
'appid' => 'app.security',
'event' => 'AUDIT_audit_failure:security_logger',
'level' => 'FATAL',
'description' => 'Security event logging failed: ' . $e->getMessage(),
'original_event_type' => $event->type->value ?? 'UNKNOWN',
'error_trace' => $e->getTraceAsString()
]);
}
}
private function logOWASPFormat(SecurityEvent $event): void
{
$securityContext = SecurityContextVO::fromGlobals();
$requestContext = RequestContext::fromGlobals();
$owaspLogFormat = $this->eventFactory->createFromSecurityEvent(
$event,
$securityContext,
$requestContext
);
// Als strukturiertes JSON loggen
$this->logger->info($owaspLogFormat->getDescription(), [
'owasp_format' => $owaspLogFormat->toArray()
]);
}
private function logLegacyFormat(SecurityEvent $event): void
{
$context = SecurityContext::fromGlobals();
$payload = $this->extractPayload($event);
$this->logger->warning($event->type->value, [
...$payload,
'ip' => $context->ip,
'user_agent' => $context->userAgent,
'timestamp' => $context->timestamp->format('c'),
]);
}
private function extractPayload(SecurityEvent $event): array
{
$data = [];
$ref = new \ReflectionObject($event);
foreach ($ref->getProperties() as $prop) {
$prop->setAccessible(true);
$data[$prop->getName()] = $prop->getValue($event);
}
return $data;
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace App\Application\Security;
enum SecurityEventType: string
{
case LOGIN_FAILED = 'auth.login.failed';
case LOGIN_SUCCEEDED = 'auth.login.succeeded';
case LOGOUT = 'auth.logout';
case PASSWORD_CHANGED = 'account.password.changed';
case EMAIL_CHANGED = 'account.email.changed';
case USER_DELETED = 'account.deleted';
case ACCESS_DENIED = 'access.denied';
case CSRF_VIOLATION = 'access.csrf.violation';
case ADMIN_ACTION = 'system.admin.action';
case CONFIG_CHANGED = 'system.config.changed';
}

View File

@@ -0,0 +1,125 @@
<?php
declare(strict_types=1);
namespace App\Application\Security\Services;
use App\Application\Security\Events\File\SuspiciousFileUploadEvent;
use App\Framework\Core\Events\EventDispatcher;
final class FileUploadSecurityService
{
private const ALLOWED_MIME_TYPES = [
'image/jpeg', 'image/png', 'image/gif', 'image/webp',
'application/pdf', 'text/plain', 'text/csv',
'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
];
private const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
private const DANGEROUS_EXTENSIONS = [
'php', 'phtml', 'php3', 'php4', 'php5', 'pl', 'py', 'jsp', 'asp', 'sh', 'cgi',
'exe', 'bat', 'com', 'scr', 'vbs', 'js', 'jar', 'war'
];
private const MALWARE_SIGNATURES = [
'eval(', 'base64_decode(', 'system(', 'exec(', 'shell_exec(',
'<?php', '<%', '<script', 'javascript:'
];
public function __construct(
private EventDispatcher $eventDispatcher
) {}
public function validateUpload(array $file): bool
{
$userEmail = $_SESSION['user_email'] ?? null;
$filename = $file['name'] ?? '';
$tmpName = $file['tmp_name'] ?? '';
$size = $file['size'] ?? 0;
$error = $file['error'] ?? UPLOAD_ERR_NO_FILE;
// Upload-Fehler prüfen
if ($error !== UPLOAD_ERR_OK) {
$this->dispatchSuspiciousUpload($filename, 'unknown', $size, 'upload_error', $userEmail);
return false;
}
// Dateigröße prüfen
if ($size > self::MAX_FILE_SIZE) {
$this->dispatchSuspiciousUpload($filename, 'unknown', $size, 'file_too_large', $userEmail);
return false;
}
// Dateiendung prüfen
$extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
if (in_array($extension, self::DANGEROUS_EXTENSIONS)) {
$this->dispatchSuspiciousUpload($filename, 'unknown', $size, 'dangerous_extension', $userEmail);
return false;
}
// MIME-Type prüfen
$mimeType = mime_content_type($tmpName);
if (!in_array($mimeType, self::ALLOWED_MIME_TYPES)) {
$this->dispatchSuspiciousUpload($filename, $mimeType, $size, 'forbidden_mime_type', $userEmail);
return false;
}
// Dateiinhalt auf Malware-Signaturen prüfen
if ($this->containsMalwareSignatures($tmpName)) {
$this->dispatchSuspiciousUpload($filename, $mimeType, $size, 'malware_signatures_detected', $userEmail);
return false;
}
// Double-Extension prüfen (z.B. file.jpg.php)
if ($this->hasDoubleExtension($filename)) {
$this->dispatchSuspiciousUpload($filename, $mimeType, $size, 'double_extension', $userEmail);
return false;
}
return true;
}
private function containsMalwareSignatures(string $filePath): bool
{
$content = file_get_contents($filePath);
if ($content === false) {
return false;
}
foreach (self::MALWARE_SIGNATURES as $signature) {
if (stripos($content, $signature) !== false) {
return true;
}
}
return false;
}
private function hasDoubleExtension(string $filename): bool
{
$parts = explode('.', $filename);
if (count($parts) < 3) {
return false;
}
// Prüfe ob vorletzte "Extension" gefährlich ist
$secondLastExtension = strtolower($parts[count($parts) - 2]);
return in_array($secondLastExtension, self::DANGEROUS_EXTENSIONS);
}
private function dispatchSuspiciousUpload(
string $filename,
string $mimeType,
int $size,
string $reason,
?string $userEmail
): void {
$this->eventDispatcher->dispatch(new SuspiciousFileUploadEvent(
filename: $filename,
mimeType: $mimeType,
fileSize: $size,
suspicionReason: $reason,
email: $userEmail
));
}
}

View File

@@ -0,0 +1,125 @@
<?php
declare(strict_types=1);
namespace App\Application\Security\Services;
use App\Application\Security\Events\Input\{
InputValidationFailureEvent,
SqlInjectionAttemptEvent,
XssAttemptEvent,
MaliciousInputDetectedEvent
};
use App\Framework\Core\Events\EventDispatcher;
final class InputValidationService
{
private const SQL_INJECTION_PATTERNS = [
'/(\bunion\b.*\bselect\b)/i',
'/(\bdrop\b.*\btable\b)/i',
'/(\binsert\b.*\binto\b)/i',
'/(\bdelete\b.*\bfrom\b)/i',
'/(\bupdate\b.*\bset\b)/i',
'/(\b(or|and)\b.*[\'"].*[\'"].*=.*[\'"].*[\'"])/i'
];
private const XSS_PATTERNS = [
'/<script[^>]*>.*?<\/script>/is',
'/<iframe[^>]*>.*?<\/iframe>/is',
'/javascript:/i',
'/on\w+\s*=/i',
'/<object[^>]*>.*?<\/object>/is'
];
public function __construct(
private EventDispatcher $eventDispatcher
) {}
public function validateInput(string $fieldName, mixed $value, array $rules = []): bool
{
$stringValue = (string) $value;
$userEmail = $_SESSION['user_email'] ?? null;
// SQL Injection Detection
if ($this->detectSqlInjection($stringValue)) {
$this->eventDispatcher->dispatch(new SqlInjectionAttemptEvent(
attackPayload: $stringValue,
targetField: $fieldName,
detectionMethod: 'pattern_matching',
email: $userEmail
));
return false;
}
// XSS Detection
if ($this->detectXss($stringValue)) {
$this->eventDispatcher->dispatch(new XssAttemptEvent(
attackPayload: $stringValue,
targetField: $fieldName,
xssType: 'reflected_xss',
email: $userEmail
));
return false;
}
// Standard Validation Rules
foreach ($rules as $rule => $parameter) {
if (!$this->applyRule($rule, $stringValue, $parameter)) {
$this->eventDispatcher->dispatch(new InputValidationFailureEvent(
fieldName: $fieldName,
invalidValue: $stringValue,
validationRule: $rule,
email: $userEmail
));
return false;
}
}
return true;
}
public function sanitizeInput(string $input, string $context = 'html'): string
{
return match ($context) {
'html' => htmlspecialchars($input, ENT_QUOTES | ENT_HTML5, 'UTF-8'),
'sql' => addslashes($input),
'url' => urlencode($input),
'javascript' => json_encode($input, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT),
default => filter_var($input, FILTER_SANITIZE_SPECIAL_CHARS)
};
}
private function detectSqlInjection(string $input): bool
{
foreach (self::SQL_INJECTION_PATTERNS as $pattern) {
if (preg_match($pattern, $input)) {
return true;
}
}
return false;
}
private function detectXss(string $input): bool
{
foreach (self::XSS_PATTERNS as $pattern) {
if (preg_match($pattern, $input)) {
return true;
}
}
return false;
}
private function applyRule(string $rule, string $value, mixed $parameter): bool
{
return match ($rule) {
'required' => !empty($value),
'email' => filter_var($value, FILTER_VALIDATE_EMAIL) !== false,
'min_length' => strlen($value) >= $parameter,
'max_length' => strlen($value) <= $parameter,
'numeric' => is_numeric($value),
'alpha' => ctype_alpha($value),
'alphanumeric' => ctype_alnum($value),
'url' => filter_var($value, FILTER_VALIDATE_URL) !== false,
default => true
};
}
}

View File

@@ -0,0 +1,121 @@
<?php
declare(strict_types=1);
namespace App\Application\Security\ValueObjects;
final class MaskedEmail
{
private function __construct(
private string $maskedValue,
private string $original
) {}
public static function fromString(string $email): self
{
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
// Nicht-E-Mail-Strings nur minimal maskieren
return new self(
self::maskGenericString($email),
$email
);
}
return new self(
self::maskEmailAddress($email),
$email
);
}
public static function fromStringWithStrategy(string $email, MaskingStrategy $strategy): self
{
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
return new self(
self::maskGenericString($email),
$email
);
}
$maskedValue = match ($strategy) {
MaskingStrategy::STANDARD => self::maskEmailAddress($email),
MaskingStrategy::PARTIAL => self::maskEmailPartial($email),
MaskingStrategy::DOMAIN_ONLY => self::maskDomainOnly($email),
MaskingStrategy::HASH => self::hashEmail($email)
};
return new self($maskedValue, $email);
}
public function getMaskedValue(): string
{
return $this->maskedValue;
}
public function getOriginal(): string
{
return $this->original;
}
public function toString(): string
{
return $this->maskedValue;
}
public function __toString(): string
{
return $this->maskedValue;
}
private static function maskEmailAddress(string $email): string
{
[$local, $domain] = explode('@', $email, 2);
if (strlen($local) <= 2) {
return str_repeat('*', strlen($local)) . '@' . $domain;
}
$maskedLocal = substr($local, 0, 2) . str_repeat('*', strlen($local) - 2);
return $maskedLocal . '@' . $domain;
}
private static function maskEmailPartial(string $email): string
{
[$local, $domain] = explode('@', $email, 2);
if (strlen($local) <= 3) {
return $local[0] . str_repeat('*', max(0, strlen($local) - 1)) . '@' . $domain;
}
$maskedLocal = $local[0] . str_repeat('*', strlen($local) - 2) . substr($local, -1);
return $maskedLocal . '@' . $domain;
}
private static function maskDomainOnly(string $email): string
{
[$local, $domain] = explode('@', $email, 2);
// Domain verschleiern, aber Local-Part zeigen
$domainParts = explode('.', $domain);
if (count($domainParts) > 1) {
$tld = array_pop($domainParts);
$maskedDomain = str_repeat('*', strlen(implode('.', $domainParts))) . '.' . $tld;
} else {
$maskedDomain = str_repeat('*', strlen($domain));
}
return $local . '@' . $maskedDomain;
}
private static function hashEmail(string $email): string
{
return 'user_' . substr(hash('sha256', $email), 0, 8);
}
private static function maskGenericString(string $input): string
{
if (strlen($input) <= 3) {
return str_repeat('*', strlen($input));
}
return substr($input, 0, 2) . str_repeat('*', strlen($input) - 2);
}
}

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace App\Application\Security\ValueObjects;
enum MaskingStrategy: string
{
case STANDARD = 'standard'; // jo***@example.com
case PARTIAL = 'partial'; // j***n@example.com
case DOMAIN_ONLY = 'domain_only'; // john@*******.com
case HASH = 'hash'; // user_a1b2c3d4
public function getDescription(): string
{
return match ($this) {
self::STANDARD => 'Standard email masking (first 2 chars + asterisks)',
self::PARTIAL => 'Partial masking (first + last char visible)',
self::DOMAIN_ONLY => 'Mask domain only, keep local part',
self::HASH => 'Replace with hash-based identifier'
};
}
}

View File

@@ -0,0 +1,119 @@
<?php
declare(strict_types=1);
namespace App\Application\Security\ValueObjects;
final class OWASPEventIdentifier
{
private function __construct(
private string $category,
private string $action,
private ?string $subject = null
) {}
public static function authenticationSuccess(string $username): self
{
return new self('AUTHN', 'login_success', $username);
}
public static function authenticationFailure(string $username): self
{
return new self('AUTHN', 'login_failure', $username);
}
public static function sessionTermination(string $username): self
{
return new self('AUTHN', 'logout', $username);
}
public static function passwordChange(string $username): self
{
return new self('AUTHN', 'password_change', $username);
}
public static function accountLocked(string $username): self
{
return new self('AUTHN', 'account_locked', $username);
}
public static function authorizationFailure(string $username, string $resource): self
{
return new self('AUTHZ', 'access_denied', "{$username}:{$resource}");
}
public static function privilegeEscalation(string $username, string $fromRole, string $toRole): self
{
return new self('AUTHZ', 'privilege_escalation', "{$username}:{$fromRole}>{$toRole}");
}
public static function inputValidationFailure(string $field): self
{
return new self('INPUT', 'validation_failure', $field);
}
public static function maliciousInput(string $attackType): self
{
return new self('INPUT', 'malicious_input', $attackType);
}
public static function fileUploadFailure(string $filename): self
{
return new self('INPUT', 'file_upload_failure', $filename);
}
public static function sessionHijacking(string $username): self
{
return new self('SESS', 'session_hijacking', $username);
}
public static function sessionTimeout(string $username): self
{
return new self('SESS', 'session_timeout', $username);
}
public static function malwareDetected(string $malwareType): self
{
return new self('MALICIOUS', 'malware_detected', $malwareType);
}
public static function auditFailure(string $eventType): self
{
return new self('AUDIT', 'audit_failure', $eventType);
}
public static function sessionFixation(string $username): self
{
return new self('SESS', 'session_fixation', $username);
}
public static function cryptographicFailure(string $operation): self
{
return new self('CRYPTO', 'crypto_failure', $operation);
}
public static function suspiciousNetworkActivity(string $activityType): self
{
return new self('NETWORK', 'suspicious_activity', $activityType);
}
public static function systemAnomaly(string $anomalyType): self
{
return new self('SYSTEM', 'anomaly_detected', $anomalyType);
}
public static function csrfViolation(string $requestPath): self
{
return new self('WEB', 'csrf_violation', $requestPath);
}
public function toString(): string
{
$identifier = "{$this->category}_{$this->action}";
if ($this->subject !== null) {
$identifier .= ":{$this->subject}";
}
return $identifier;
}
}

View File

@@ -0,0 +1,110 @@
<?php
declare(strict_types=1);
namespace App\Application\Security\ValueObjects;
use DateTimeImmutable;
use DateTimeInterface;
final class OWASPLogFormat
{
public function __construct(
private DateTimeImmutable $datetime,
private string $appid,
private string $event,
private string $level,
private string $description,
private ?string $useragent = null,
private ?string $sourceIp = null,
private ?string $hostIp = null,
private ?string $hostname = null,
private ?string $protocol = null,
private ?string $port = null,
private ?string $requestUri = null,
private ?string $requestMethod = null,
private ?string $region = null,
private ?string $geo = null
) {}
public static function create(
string $appid,
string $event,
string $level,
string $description,
SecurityContext $context,
RequestContext $requestContext
): self {
return new self(
new DateTimeImmutable(),
$appid,
$event,
$level,
$description,
$context->getUserAgent(),
$context->getSourceIp(),
$requestContext->getHostIp(),
$requestContext->getHostname(),
$requestContext->getProtocol(),
$requestContext->getPort(),
$requestContext->getRequestUri(),
$requestContext->getRequestMethod(),
$requestContext->getRegion(),
$requestContext->getGeo()
);
}
public function toArray(): array
{
$data = [
'datetime' => $this->datetime->format(DateTimeInterface::ATOM),
'appid' => $this->appid,
'event' => $this->event,
'level' => $this->level,
'description' => $this->description
];
// Nur nicht-null Werte hinzufügen
if ($this->useragent !== null) {
$data['useragent'] = $this->useragent;
}
if ($this->sourceIp !== null) {
$data['source_ip'] = $this->sourceIp;
}
if ($this->hostIp !== null) {
$data['host_ip'] = $this->hostIp;
}
if ($this->hostname !== null) {
$data['hostname'] = $this->hostname;
}
if ($this->protocol !== null) {
$data['protocol'] = $this->protocol;
}
if ($this->port !== null) {
$data['port'] = $this->port;
}
if ($this->requestUri !== null) {
$data['request_uri'] = $this->requestUri;
}
if ($this->requestMethod !== null) {
$data['request_method'] = $this->requestMethod;
}
if ($this->region !== null) {
$data['region'] = $this->region;
}
if ($this->geo !== null) {
$data['geo'] = $this->geo;
}
return $data;
}
public function toJson(): string
{
return json_encode($this->toArray(), JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES);
}
public function getDescription(): string
{
return $this->description;
}
}

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace App\Application\Security\ValueObjects;
use App\Application\Security\SecurityEventType;
enum OWASPLogLevel: string
{
case DEBUG = 'DEBUG';
case INFO = 'INFO';
case WARN = 'WARN';
case ERROR = 'ERROR';
case FATAL = 'FATAL';
public static function fromSecurityEventType(SecurityEventType $type): self
{
return match ($type) {
SecurityEventType::LOGIN_SUCCESS,
SecurityEventType::LOGOUT,
SecurityEventType::PASSWORD_CHANGE => self::INFO,
SecurityEventType::LOGIN_FAILED,
SecurityEventType::ACCESS_DENIED,
SecurityEventType::SESSION_TIMEOUT => self::WARN,
SecurityEventType::INJECTION_ATTEMPT,
SecurityEventType::MALWARE_DETECTED,
SecurityEventType::ACCOUNT_LOCKED,
SecurityEventType::FILE_UPLOAD => self::ERROR,
SecurityEventType::PRIVILEGE_ESCALATION,
SecurityEventType::SESSION_HIJACK,
SecurityEventType::AUDIT_FAILURE => self::FATAL,
default => self::INFO
};
}
}

View File

@@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace App\Application\Security\ValueObjects;
final class RequestContext
{
private function __construct(
private ?string $hostIp,
private ?string $hostname,
private ?string $protocol,
private ?string $port,
private ?string $requestUri,
private ?string $requestMethod,
private ?string $region,
private ?string $geo
) {}
public static function fromGlobals(): self
{
return new self(
$_SERVER['SERVER_ADDR'] ?? null,
$_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'] ?? null,
isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' ? 'https' : 'http',
$_SERVER['SERVER_PORT'] ?? null,
$_SERVER['REQUEST_URI'] ?? null,
$_SERVER['REQUEST_METHOD'] ?? null,
$_ENV['AWS_REGION'] ?? $_ENV['REGION'] ?? null,
$_ENV['GEO_REGION'] ?? null
);
}
public function getHostIp(): ?string
{
return $this->hostIp;
}
public function getHostname(): ?string
{
return $this->hostname;
}
public function getProtocol(): ?string
{
return $this->protocol;
}
public function getPort(): ?string
{
return $this->port;
}
public function getRequestUri(): ?string
{
return $this->requestUri;
}
public function getRequestMethod(): ?string
{
return $this->requestMethod;
}
public function getRegion(): ?string
{
return $this->region;
}
public function getGeo(): ?string
{
return $this->geo;
}
}

View File

@@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
namespace App\Application\Security\ValueObjects;
use DateTimeImmutable;
final class SecurityContext
{
private function __construct(
private ?string $sourceIp,
private ?string $userAgent,
private ?string $sessionId,
private ?string $requestId,
private ?string $userId,
private DateTimeImmutable $timestamp
) {}
public static function fromGlobals(): self
{
return new self(
$_SERVER['REMOTE_ADDR'] ?? null,
$_SERVER['HTTP_USER_AGENT'] ?? null,
session_id() ?: null,
$_SERVER['HTTP_X_REQUEST_ID'] ?? null,
$_SESSION['user_id'] ?? null,
new DateTimeImmutable()
);
}
public static function create(
?string $sourceIp = null,
?string $userAgent = null,
?string $sessionId = null,
?string $requestId = null,
?string $userId = null
): self {
return new self(
$sourceIp,
$userAgent,
$sessionId,
$requestId,
$userId,
new DateTimeImmutable()
);
}
public function getSourceIp(): ?string
{
return $this->sourceIp;
}
public function getUserAgent(): ?string
{
return $this->userAgent;
}
public function getSessionId(): ?string
{
return $this->sessionId;
}
public function getRequestId(): ?string
{
return $this->requestId;
}
public function getUserId(): ?string
{
return $this->userId;
}
public function getTimestamp(): DateTimeImmutable
{
return $this->timestamp;
}
}