chore: complete update
This commit is contained in:
21
src/Application/Security/Events/Access/CsrfViolation.php
Normal file
21
src/Application/Security/Events/Access/CsrfViolation.php
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
51
src/Application/Security/Events/Auth/AccountLockedEvent.php
Normal file
51
src/Application/Security/Events/Auth/AccountLockedEvent.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
20
src/Application/Security/Events/Auth/LoginFailed.php
Normal file
20
src/Application/Security/Events/Auth/LoginFailed.php
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
57
src/Application/Security/Events/Input/XssAttemptEvent.php
Normal file
57
src/Application/Security/Events/Input/XssAttemptEvent.php
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
];
|
||||
}
|
||||
}
|
||||
53
src/Application/Security/Events/Web/CsrfViolationEvent.php
Normal file
53
src/Application/Security/Events/Web/CsrfViolationEvent.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
129
src/Application/Security/Guards/AuthenticationGuard.php
Normal file
129
src/Application/Security/Guards/AuthenticationGuard.php
Normal 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);
|
||||
}
|
||||
}
|
||||
106
src/Application/Security/Middleware/SecurityEventMiddleware.php
Normal file
106
src/Application/Security/Middleware/SecurityEventMiddleware.php
Normal 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';
|
||||
}
|
||||
}
|
||||
29
src/Application/Security/OWASPSecurityEvent.php
Normal file
29
src/Application/Security/OWASPSecurityEvent.php
Normal 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;
|
||||
}
|
||||
210
src/Application/Security/OWASPSecurityEventFactory.php
Normal file
210
src/Application/Security/OWASPSecurityEventFactory.php
Normal 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;
|
||||
}
|
||||
}
|
||||
89
src/Application/Security/OWASPSecurityEventLogger.php
Normal file
89
src/Application/Security/OWASPSecurityEventLogger.php
Normal 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'
|
||||
};
|
||||
}
|
||||
}
|
||||
53
src/Application/Security/SecurityContext.php
Normal file
53
src/Application/Security/SecurityContext.php
Normal 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))
|
||||
);
|
||||
}
|
||||
}
|
||||
10
src/Application/Security/SecurityEvent.php
Normal file
10
src/Application/Security/SecurityEvent.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Application\Security;
|
||||
|
||||
interface SecurityEvent
|
||||
{
|
||||
public SecurityEventType $type {
|
||||
get;
|
||||
}
|
||||
}
|
||||
84
src/Application/Security/SecurityEventLogger.php
Normal file
84
src/Application/Security/SecurityEventLogger.php
Normal 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;
|
||||
}
|
||||
}
|
||||
20
src/Application/Security/SecurityEventType.php
Normal file
20
src/Application/Security/SecurityEventType.php
Normal 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';
|
||||
}
|
||||
125
src/Application/Security/Services/FileUploadSecurityService.php
Normal file
125
src/Application/Security/Services/FileUploadSecurityService.php
Normal 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
|
||||
));
|
||||
}
|
||||
}
|
||||
125
src/Application/Security/Services/InputValidationService.php
Normal file
125
src/Application/Security/Services/InputValidationService.php
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
121
src/Application/Security/ValueObjects/MaskedEmail.php
Normal file
121
src/Application/Security/ValueObjects/MaskedEmail.php
Normal 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);
|
||||
}
|
||||
}
|
||||
22
src/Application/Security/ValueObjects/MaskingStrategy.php
Normal file
22
src/Application/Security/ValueObjects/MaskingStrategy.php
Normal 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'
|
||||
};
|
||||
}
|
||||
}
|
||||
119
src/Application/Security/ValueObjects/OWASPEventIdentifier.php
Normal file
119
src/Application/Security/ValueObjects/OWASPEventIdentifier.php
Normal 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;
|
||||
}
|
||||
}
|
||||
110
src/Application/Security/ValueObjects/OWASPLogFormat.php
Normal file
110
src/Application/Security/ValueObjects/OWASPLogFormat.php
Normal 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;
|
||||
}
|
||||
}
|
||||
39
src/Application/Security/ValueObjects/OWASPLogLevel.php
Normal file
39
src/Application/Security/ValueObjects/OWASPLogLevel.php
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
72
src/Application/Security/ValueObjects/RequestContext.php
Normal file
72
src/Application/Security/ValueObjects/RequestContext.php
Normal 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;
|
||||
}
|
||||
}
|
||||
77
src/Application/Security/ValueObjects/SecurityContext.php
Normal file
77
src/Application/Security/ValueObjects/SecurityContext.php
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user