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,43 @@
<?php
declare(strict_types=1);
namespace App\Framework\Exception;
final class ConsoleException extends FrameworkException
{
public static function commandFailed(string $command, int $exitCode, string $output = ''): self
{
$context = ExceptionContext::forOperation('console.command', 'ConsoleRunner')
->withData([
'command' => $command,
'exit_code' => $exitCode,
'output' => $output,
'working_directory' => getcwd()
]);
return new self("Console command failed: {$command}", $exitCode, null, $context);
}
public static function commandNotFound(string $command): self
{
$context = ExceptionContext::forOperation('console.command_lookup', 'ConsoleRunner')
->withData([
'command' => $command
]);
return new self("Console command not found: {$command}", 404, null, $context);
}
public static function invalidArguments(string $command, array $arguments, array $errors): self
{
$context = ExceptionContext::forOperation('console.argument_validation', 'ConsoleRunner')
->withData([
'command' => $command,
'arguments' => $arguments,
'validation_errors' => $errors
]);
return new self("Invalid arguments for command: {$command}", 400, null, $context);
}
}

View File

@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace App\Framework\Exception;
final class DatabaseException extends FrameworkException
{
public static function connectionFailed(string $dsn, \Throwable $previous): self
{
$context = ExceptionContext::forOperation('database.connect', 'PDO')
->withData([
'dsn' => self::sanitizeDsn($dsn),
'driver' => explode(':', $dsn)[0] ?? 'unknown'
]);
return new self('Database connection failed', 0, $previous, $context);
}
public static function queryFailed(string $query, array $params, \Throwable $previous): self
{
$context = ExceptionContext::forOperation('database.query', 'PDO')
->withData([
'query' => $query,
'parameters' => $params,
'query_type' => self::detectQueryType($query)
]);
return new self('Database query failed', 0, $previous, $context);
}
public static function transactionFailed(string $operation, \Throwable $previous): self
{
$context = ExceptionContext::forOperation('database.transaction', 'PDO')
->withData([
'transaction_operation' => $operation
]);
return new self("Database transaction failed: {$operation}", 0, $previous, $context);
}
private static function sanitizeDsn(string $dsn): string
{
return preg_replace('/password=[^;]+/', 'password=[REDACTED]', $dsn);
}
private static function detectQueryType(string $query): string
{
$query = trim(strtoupper($query));
return match (true) {
str_starts_with($query, 'SELECT') => 'SELECT',
str_starts_with($query, 'INSERT') => 'INSERT',
str_starts_with($query, 'UPDATE') => 'UPDATE',
str_starts_with($query, 'DELETE') => 'DELETE',
str_starts_with($query, 'CREATE') => 'CREATE',
str_starts_with($query, 'DROP') => 'DROP',
str_starts_with($query, 'ALTER') => 'ALTER',
default => 'UNKNOWN'
};
}
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace App\Framework\Exception;
final class DirectoryCreateException extends FrameworkException
{
public function __construct(
string $directory,
int $code = 0,
?\Throwable $previous = null
) {
$context = ExceptionContext::forOperation('filesystem.create_directory', 'FileSystem')
->withData([
'directory' => $directory,
'permissions' => is_dir(dirname($directory)) ? decoct(fileperms(dirname($directory))) : 'unknown',
'parent_exists' => is_dir(dirname($directory)),
'parent_writable' => is_writable(dirname($directory)),
'disk_free_space' => disk_free_space(dirname($directory))
]);
parent::__construct(
message: "Ordner '$directory' konnte nicht angelegt werden.",
code: $code,
previous: $previous,
context: $context
);
}
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace App\Framework\Exception;
final class DirectoryListException extends FrameworkException
{
public function __construct(
string $directory,
int $code = 0,
?\Throwable $previous = null
) {
$context = ExceptionContext::forOperation('filesystem.list_directory', 'FileSystem')
->withData([
'directory' => $directory,
'exists' => is_dir($directory),
'readable' => is_readable($directory),
'permissions' => is_dir($directory) ? decoct(fileperms($directory)) : 'unknown',
'file_count' => is_readable($directory) ? count(scandir($directory)) - 2 : 'unknown'
]);
parent::__construct(
message: "Fehler beim Auslesen des Verzeichnisses '$directory'.",
code: $code,
previous: $previous,
context: $context
);
}
}

View File

@@ -0,0 +1,208 @@
<?php
declare(strict_types=1);
namespace App\Framework\Exception;
use App\Framework\Http\MiddlewareContext;
/**
* Vollständiger Kontext für Error-Handling - kombiniert alle Kontext-Typen
*/
final readonly class ErrorHandlerContext
{
public function __construct(
public ExceptionContext $exception,
public RequestContext $request,
public SystemContext $system,
public array $metadata = []
) {}
public static function create(
ExceptionContext $exceptionContext,
?RequestContext $requestContext = null,
?SystemContext $systemContext = null,
array $metadata = []
): self {
return new self(
exception: $exceptionContext,
request: $requestContext ?? RequestContext::fromGlobals(),
system: $systemContext ?? SystemContext::current(),
metadata: $metadata
);
}
public static function fromException(\Throwable $exception, array $metadata = []): self
{
$exceptionContext = ExceptionContext::empty();
if ($exception instanceof FrameworkException) {
$exceptionContext = $exception->getContext();
}
return self::create($exceptionContext, metadata: $metadata);
}
public function withMetadata(array $metadata): self
{
return new self(
exception: $this->exception,
request: $this->request,
system: $this->system,
metadata: array_merge($this->metadata, $metadata)
);
}
public function withRequest(RequestContext $request): self
{
return new self(
exception: $this->exception,
request: $request,
system: $this->system,
metadata: $this->metadata
);
}
public function withSystem(SystemContext $system): self
{
return new self(
exception: $this->exception,
request: $this->request,
system: $system,
metadata: $this->metadata
);
}
public function toArray(): array
{
return [
'exception' => $this->exception->toArray(),
'request' => $this->request->toArray(),
'system' => $this->system->toArray(),
'metadata' => $this->metadata
];
}
/**
* Gibt eine flache Struktur zurück für einfache Serialisierung
*/
public function toFlatArray(): array
{
$contexts = [
'exception' => $this->exception->toArray(),
'request' => $this->request->toArray(),
'system' => $this->system->toArray()
];
$flattened = [];
foreach ($contexts as $prefix => $data) {
array_walk($data, function($value, $key) use (&$flattened, $prefix) {
$flattened[$prefix . '_' . $key] = $value;
});
}
return array_merge($flattened, $this->metadata);
}
/**
* Extrahiert nur relevante Daten für Logging
*/
public function forLogging(): array
{
return [
'operation' => $this->exception->operation,
'component' => $this->exception->component,
'client_ip' => $this->request->clientIp,
'request_method' => $this->request->requestMethod,
'request_uri' => $this->request->requestUri,
'memory_usage' => $this->system->memoryUsage,
'execution_time' => $this->system->executionTime,
'data' => $this->exception->data,
'metadata' => $this->metadata
];
}
/**
* Generiert Security-Event-Log im OWASP-Format
*/
public function toSecurityEventFormat(string $appId = 'app'): array
{
$exceptionData = $this->exception->toArray();
// Basis-Format
$securityLog = [
'datetime' => date('c'),
'appid' => $appId,
'level' => 'INFO',
'description' => 'No description',
'useragent' => $this->request->userAgent,
'source_ip' => $this->request->clientIp,
'host_ip' => $this->request->hostIp,
'hostname' => $this->request->hostname,
'protocol' => $this->request->protocol,
'port' => $this->request->port,
'request_uri' => $this->request->requestUri,
'request_method' => $this->request->requestMethod,
'region' => $_ENV['AWS_REGION'] ?? 'unknown',
'geo' => $_ENV['GEO_LOCATION'] ?? 'unknown'
];
// Security-Event-spezifische Daten falls verfügbar
if (isset($exceptionData['metadata']['security_event'])) {
$securityLog['event'] = $exceptionData['metadata']['security_event'];
$securityLog['level'] = $exceptionData['metadata']['security_level'] ?? 'INFO';
$securityLog['description'] = $exceptionData['metadata']['security_description'] ?? 'Security event';
} else {
// Fallback für normale Exceptions
$securityLog['event'] = 'application_error';
$securityLog['level'] = 'ERROR';
$securityLog['description'] = 'Application error occurred';
}
return $securityLog;
}
/**
* Generiert JSON für Security-Event-Log
*/
public function toSecurityEventJson(string $appId = 'app'): string
{
return json_encode($this->toSecurityEventFormat($appId), JSON_UNESCAPED_SLASHES);
}
/**
* Factory-Methode um ErrorHandlerContext aus Exception zu erstellen
*/
/*public static function fromException(Throwable $exception, ?MiddlewareContext $middlewareContext = null): self
{
// Request-Context aufbauen
$requestContext = RequestContext::create(
clientIp: $_SERVER['REMOTE_ADDR'] ?? 'unknown',
userAgent: $_SERVER['HTTP_USER_AGENT'] ?? 'unknown',
requestMethod: $_SERVER['REQUEST_METHOD'] ?? 'CLI',
requestUri: $_SERVER['REQUEST_URI'] ?? '-',
hostIp: $_SERVER['SERVER_ADDR'] ?? 'unknown',
hostname: $_SERVER['HTTP_HOST'] ?? 'localhost',
protocol: $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.1',
port: $_SERVER['SERVER_PORT'] ?? '80',
requestId: $middlewareContext?->requestId ?? bin2hex(random_bytes(8))
);
// Exception-Context
$exceptionContext = ExceptionContext::fromThrowable($exception);
// System-Context
$systemContext = SystemContext::create(
memoryUsage: memory_get_usage(true),
peakMemoryUsage: memory_get_peak_usage(true),
executionTime: microtime(true) - ($_SERVER['REQUEST_TIME_FLOAT'] ?? microtime(true))
);
return new self(
exception: $exceptionContext,
request: $requestContext,
system: $systemContext,
metadata: []
);
}*/
}

View File

@@ -0,0 +1,97 @@
<?php
declare(strict_types=1);
namespace App\Framework\Exception;
/**
* Fachlicher Kontext für Exceptions - nur domain-spezifische Informationen
*/
final readonly class ExceptionContext
{
public function __construct(
public ?string $operation = null,
public ?string $component = null,
public array $data = [],
public array $debug = [],
public array $metadata = []
) {}
public static function empty(): self
{
return new self();
}
public static function forOperation(string $operation, ?string $component = null): self
{
return new self(operation: $operation, component: $component);
}
public function withOperation(string $operation, ?string $component = null): self
{
return new self(
operation: $operation,
component: $component ?? $this->component,
data: $this->data,
debug: $this->debug,
metadata: $this->metadata
);
}
public function withData(array $data): self
{
return new self(
operation: $this->operation,
component: $this->component,
data: array_merge($this->data, $data),
debug: $this->debug,
metadata: $this->metadata
);
}
public function withDebug(array $debug): self
{
return new self(
operation: $this->operation,
component: $this->component,
data: $this->data,
debug: array_merge($this->debug, $debug),
metadata: $this->metadata
);
}
public function withMetadata(array $metadata): self
{
return new self(
operation: $this->operation,
component: $this->component,
data: $this->data,
debug: $this->debug,
metadata: array_merge($this->metadata, $metadata)
);
}
public function toArray(): array
{
return [
'operation' => $this->operation,
'component' => $this->component,
'data' => $this->sanitizeData($this->data),
'debug' => $this->debug,
'metadata' => $this->metadata
];
}
private function sanitizeData(array $data): array
{
$sensitiveKeys = ['password', 'token', 'api_key', 'secret', 'private_key'];
array_walk_recursive($data, function (&$value, $key) use ($sensitiveKeys) {
if (in_array(strtolower($key), $sensitiveKeys)) {
$value = '[REDACTED]';
}
});
return $data;
}
}

View File

@@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace App\Framework\Exception;
class FrameworkException extends \RuntimeException
{
protected ExceptionContext $context;
public function __construct(
string $message,
int $code = 0,
?\Throwable $previous = null,
?ExceptionContext $context = null
) {
parent::__construct($message, $code, $previous);
$this->context = $context ?? ExceptionContext::empty();
}
public function getContext(): ExceptionContext
{
return $this->context;
}
public function withContext(ExceptionContext $context): self
{
$new = clone $this;
$new->context = $context;
return $new;
}
public function withOperation(string $operation, ?string $component = null): self
{
return $this->withContext(
$this->context->withOperation($operation, $component)
);
}
public function withData(array $data): self
{
return $this->withContext(
$this->context->withData($data)
);
}
public function withDebug(array $debug): self
{
return $this->withContext(
$this->context->withDebug($debug)
);
}
public function withMetadata(array $metadata): self
{
return $this->withContext(
$this->context->withMetadata($metadata)
);
}
public function toArray(): array
{
return [
'class' => static::class,
'message' => $this->getMessage(),
'code' => $this->getCode(),
'file' => $this->getFile(),
'line' => $this->getLine(),
'context' => $this->context->toArray(),
'trace' => $this->getTraceAsString(),
];
}
}

View File

@@ -0,0 +1,22 @@
```` php
// Im ErrorHandler wird jetzt ErrorHandlerContext verwendet
private function createErrorContext(Throwable $exception, ?MiddlewareContext $context = null): ErrorContext
{
$handlerContext = ErrorHandlerContext::fromException($exception, [
'request_id' => $context?->requestId ?? $this->requestIdGenerator->generate(),
'timestamp' => date('c'),
'environment' => $_ENV['APP_ENV'] ?? 'production',
'debug_mode' => $this->isDebugMode
]);
return new ErrorContext(
exception: $exception,
level: $this->determineErrorLevel($exception),
requestId: $context?->requestId ?? $this->requestIdGenerator->generate(),
context: $handlerContext, // Jetzt ErrorHandlerContext statt ExceptionContext
additionalData: [
'memory_usage' => memory_get_peak_usage(true),
]
);
}
````

View File

@@ -0,0 +1,95 @@
<?php
declare(strict_types=1);
namespace App\Framework\Exception;
final readonly class RequestContext
{
public function __construct(
public ?string $hostIp = null,
public ?string $hostname = null,
public ?string $protocol = null,
public ?string $port = null,
public ?string $requestUri = null,
public ?string $requestMethod = null,
public ?string $userAgent = null,
public ?string $clientIp = null,
public array $headers = []
) {}
public static function fromGlobals(): self
{
return new self(
hostIp: $_SERVER['SERVER_ADDR'] ?? null,
hostname: $_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'] ?? null,
protocol: isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' ? 'https' : 'http',
port: $_SERVER['SERVER_PORT'] ?? null,
requestUri: $_SERVER['REQUEST_URI'] ?? null,
requestMethod: $_SERVER['REQUEST_METHOD'] ?? null,
userAgent: $_SERVER['HTTP_USER_AGENT'] ?? null,
clientIp: self::getClientIp(),
headers: self::getHeaders()
);
}
public static function empty(): self
{
return new self();
}
public function toArray(): array
{
return [
'host_ip' => $this->hostIp,
'hostname' => $this->hostname,
'protocol' => $this->protocol,
'port' => $this->port,
'request_uri' => $this->requestUri,
'request_method' => $this->requestMethod,
'user_agent' => $this->userAgent,
'client_ip' => $this->clientIp,
'headers' => $this->headers
];
}
private static function getClientIp(): ?string
{
$ipKeys = [
'HTTP_CF_CONNECTING_IP', // Cloudflare
'HTTP_X_FORWARDED_FOR', // Load balancer/proxy
'HTTP_X_FORWARDED', // Proxy
'HTTP_X_CLUSTER_CLIENT_IP', // Cluster
'HTTP_FORWARDED_FOR', // Proxy
'HTTP_FORWARDED', // Proxy
'REMOTE_ADDR' // Standard
];
foreach ($ipKeys as $key) {
if (!empty($_SERVER[$key])) {
$ips = explode(',', $_SERVER[$key]);
$ip = trim($ips[0]);
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
return $ip;
}
}
}
return $_SERVER['REMOTE_ADDR'] ?? null;
}
private static function getHeaders(): array
{
$headers = [];
foreach ($_SERVER as $key => $value) {
if (str_starts_with($key, 'HTTP_')) {
$header = str_replace('_', '-', substr($key, 5));
$header = ucwords(strtolower($header), '-');
$headers[$header] = $value;
}
}
return $headers;
}
}

View File

@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace App\Framework\Exception\SecurityEvent;
use App\Framework\Exception\SecurityLogLevel;
/**
* Account-Sperrung Event
*/
final readonly class AuthenticationAccountLockedEvent implements SecurityEventInterface
{
public function __construct(
public string $userId,
public int $attempts
) {}
public function getEventIdentifier(): string
{
return "authn_account_locked:{$this->userId},{$this->attempts}";
}
public function getDescription(): string
{
return "User {$this->userId} account locked after {$this->attempts} failed attempts";
}
public function getLogLevel(): SecurityLogLevel
{
return SecurityLogLevel::WARN;
}
public function getCategory(): string
{
return 'authentication';
}
public function requiresAlert(): bool
{
// Account-Sperrungen sind immer alert-würdig
return true;
}
public function toArray(): array
{
return [
'userId' => $this->userId,
'attempts' => $this->attempts,
'event_identifier' => $this->getEventIdentifier(),
'category' => $this->getCategory(),
'log_level' => $this->getLogLevel()->value,
'requires_alert' => $this->requiresAlert()
];
}
}

View File

@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace App\Framework\Exception\SecurityEvent;
use App\Framework\Exception\SecurityLogLevel;
/**
* Login-Fehler Event
*/
final readonly class AuthenticationLoginFailedEvent implements SecurityEventInterface
{
public function __construct(
public string $userId,
public string $reason = 'invalid_credentials'
) {}
public function getEventIdentifier(): string
{
return "authn_login_fail:{$this->userId}";
}
public function getDescription(): string
{
return "User {$this->userId} login failure";
}
public function getLogLevel(): SecurityLogLevel
{
return SecurityLogLevel::WARN;
}
public function getCategory(): string
{
return 'authentication';
}
public function requiresAlert(): bool
{
// Login-Fehler sind nicht kritisch genug für sofortige Alerts
return false;
}
public function toArray(): array
{
return [
'userId' => $this->userId,
'reason' => $this->reason,
'event_identifier' => $this->getEventIdentifier(),
'category' => $this->getCategory(),
'log_level' => $this->getLogLevel()->value,
'requires_alert' => $this->requiresAlert()
];
}
}

View File

@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace App\Framework\Exception\SecurityEvent;
use App\Framework\Exception\SecurityLogLevel;
/**
* Login-Erfolg nach Fehlversuchen Event
*/
final readonly class AuthenticationLoginSuccessAfterFailEvent implements SecurityEventInterface
{
public function __construct(
public string $userId,
public int $retries
) {}
public function getEventIdentifier(): string
{
return "authn_login_successafterfail:{$this->userId},{$this->retries}";
}
public function getDescription(): string
{
return "User {$this->userId} login successfully after {$this->retries} failures";
}
public function getLogLevel(): SecurityLogLevel
{
return SecurityLogLevel::WARN;
}
public function getCategory(): string
{
return 'authentication';
}
public function requiresAlert(): bool
{
// Alert wenn viele Fehlversuche vorausgingen
return $this->retries >= 5;
}
public function toArray(): array
{
return [
'userId' => $this->userId,
'retries' => $this->retries,
'event_identifier' => $this->getEventIdentifier(),
'category' => $this->getCategory(),
'log_level' => $this->getLogLevel()->value,
'requires_alert' => $this->requiresAlert()
];
}
}

View File

@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace App\Framework\Exception\SecurityEvent;
use App\Framework\Exception\SecurityLogLevel;
/**
* Login-Erfolg Event
*/
final readonly class AuthenticationLoginSuccessEvent implements SecurityEventInterface
{
public function __construct(
public string $userId
) {}
public function getEventIdentifier(): string
{
return "authn_login_success:{$this->userId}";
}
public function getDescription(): string
{
return "User {$this->userId} login successfully";
}
public function getLogLevel(): SecurityLogLevel
{
return SecurityLogLevel::INFO;
}
public function getCategory(): string
{
return 'authentication';
}
public function requiresAlert(): bool
{
return false;
}
public function toArray(): array
{
return [
'userId' => $this->userId,
'event_identifier' => $this->getEventIdentifier(),
'category' => $this->getCategory(),
'log_level' => $this->getLogLevel()->value,
'requires_alert' => $this->requiresAlert()
];
}
}

View File

@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace App\Framework\Exception\SecurityEvent;
use App\Framework\Exception\SecurityLogLevel;
/**
* Zugriff verweigert Event
*/
final readonly class AuthorizationAccessDeniedEvent implements SecurityEventInterface
{
public function __construct(
public string $userId,
public string $resource,
public string $action = 'access'
) {}
public function getEventIdentifier(): string
{
return "authz_fail:{$this->userId},{$this->resource}";
}
public function getDescription(): string
{
return "User {$this->userId} authorization failure for {$this->resource}";
}
public function getLogLevel(): SecurityLogLevel
{
return SecurityLogLevel::WARN;
}
public function getCategory(): string
{
return 'authorization';
}
public function requiresAlert(): bool
{
return false;
}
public function toArray(): array
{
return [
'userId' => $this->userId,
'resource' => $this->resource,
'action' => $this->action,
'event_identifier' => $this->getEventIdentifier(),
'category' => $this->getCategory(),
'log_level' => $this->getLogLevel()->value,
'requires_alert' => $this->requiresAlert()
];
}
}

View File

@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace App\Framework\Exception\SecurityEvent;
use App\Framework\Exception\SecurityLogLevel;
/**
* Admin-Aktion Event
*/
final readonly class AuthorizationAdminActionEvent implements SecurityEventInterface
{
public function __construct(
public string $userId,
public string $resource,
public string $action = 'admin_action'
) {}
public function getEventIdentifier(): string
{
return "authz_admin:{$this->userId},{$this->resource}";
}
public function getDescription(): string
{
return "User {$this->userId} administrative action on {$this->resource}";
}
public function getLogLevel(): SecurityLogLevel
{
return SecurityLogLevel::WARN;
}
public function getCategory(): string
{
return 'authorization';
}
public function requiresAlert(): bool
{
// Admin-Aktionen sind immer beobachtungswürdig
return true;
}
public function toArray(): array
{
return [
'userId' => $this->userId,
'resource' => $this->resource,
'action' => $this->action,
'event_identifier' => $this->getEventIdentifier(),
'category' => $this->getCategory(),
'log_level' => $this->getLogLevel()->value,
'requires_alert' => $this->requiresAlert()
];
}
}

View File

@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace App\Framework\Exception\SecurityEvent;
use App\Framework\Exception\SecurityException;
use App\Framework\Exception\SecurityLogLevel;
/**
* SQL-Injection-Versuch Event
*/
final readonly class InputSqlInjectionAttemptEvent implements SecurityEventInterface
{
public function __construct(
public string $field,
public string $detectedPattern = 'generic_sql_pattern'
) {}
public function getEventIdentifier(): string
{
return "input_sql_injection:{$this->field}";
}
public function getDescription(): string
{
return "SQL injection attempt detected in field {$this->field}";
}
public function getLogLevel(): SecurityLogLevel
{
return SecurityLogLevel::ERROR;
}
public function getCategory(): string
{
return 'input_validation';
}
public function requiresAlert(): bool
{
// SQL-Injection-Versuche sind immer kritisch
return true;
}
public function toArray(): array
{
return [
'field' => $this->field,
'detectedPattern' => $this->detectedPattern,
'event_identifier' => $this->getEventIdentifier(),
'category' => $this->getCategory(),
'log_level' => $this->getLogLevel()->value,
'requires_alert' => $this->requiresAlert()
];
}
}

View File

@@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace App\Framework\Exception\SecurityEvent;
use App\Framework\Exception\SecurityLogLevel;
/**
* XSS-Versuch Event
*/
final readonly class InputXssAttemptEvent implements SecurityEventInterface
{
public function __construct(
public string $field,
public string $detectedPattern = 'generic_xss_pattern'
) {}
public function getEventIdentifier(): string
{
return "input_xss_attempt:{$this->field}";
}
public function getDescription(): string
{
return "XSS attack attempt detected in field {$this->field}";
}
public function getLogLevel(): SecurityLogLevel
{
return SecurityLogLevel::WARN;
}
public function getCategory(): string
{
return 'input_validation';
}
public function requiresAlert(): bool
{
return true;
}
public function toArray(): array
{
return [
'field' => $this->field,
'detectedPattern' => $this->detectedPattern,
'event_identifier' => $this->getEventIdentifier(),
'category' => $this->getCategory(),
'log_level' => $this->getLogLevel()->value,
'requires_alert' => $this->requiresAlert()
];
}
}

View File

@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace App\Framework\Exception\SecurityEvent;
use App\Framework\Exception\SecurityLogLevel;
/**
* Interface für alle Security-Event Value Objects
*/
interface SecurityEventInterface
{
/**
* Gibt den OWASP-Event-Identifier zurück (z.B. "authn_login_fail:user123")
*/
public function getEventIdentifier(): string;
/**
* Gibt die OWASP-Beschreibung zurück (z.B. "User user123 login failure")
*/
public function getDescription(): string;
/**
* Gibt das Log-Level zurück
*/
public function getLogLevel(): SecurityLogLevel;
/**
* Prüft ob Event kritisch ist und Alerting erfordert
*/
public function requiresAlert(): bool;
/**
* Gibt die Event-Daten als Array zurück
*/
public function toArray(): array;
/**
* Gibt die Event-Kategorie zurück (auth, authz, input, etc.)
*/
public function getCategory(): string;
}

View File

@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace App\Framework\Exception\SecurityEvent;
use App\Framework\Exception\SecurityLogLevel;
/**
* Exzessive Nutzung Event
*/
final readonly class SystemExcessiveUseEvent implements SecurityEventInterface
{
public function __construct(
public string $identifier,
public int $limit,
public int $currentUsage
) {}
public function getEventIdentifier(): string
{
return "excessive_use:{$this->identifier},{$this->limit}";
}
public function getDescription(): string
{
return "Excessive usage detected for {$this->identifier} exceeding limit {$this->limit}";
}
public function getLogLevel(): SecurityLogLevel
{
return SecurityLogLevel::WARN;
}
public function getCategory(): string
{
return 'system';
}
public function requiresAlert(): bool
{
// Rate-Limiting-Verletzungen sind immer alert-würdig
return true;
}
public function toArray(): array
{
return [
'identifier' => $this->identifier,
'limit' => $this->limit,
'currentUsage' => $this->currentUsage,
'event_identifier' => $this->getEventIdentifier(),
'category' => $this->getCategory(),
'log_level' => $this->getLogLevel()->value,
'requires_alert' => $this->requiresAlert()
];
}
}

View File

@@ -0,0 +1,103 @@
<?php
declare(strict_types=1);
namespace App\Framework\Exception;
use App\Framework\Exception\SecurityEvent\SecurityEventInterface;
/**
* Security-spezifische Exception mit OWASP-konformen Event-Logging
*/
class SecurityException extends FrameworkException
{
protected SecurityEventInterface $securityEvent;
public function __construct(
SecurityEventInterface $securityEvent,
string $message = '',
int $code = 0,
?\Throwable $previous = null,
?ExceptionContext $additionalContext = null
) {
$this->securityEvent = $securityEvent;
// Erstelle Security-Context
$context = $this->createSecurityContext($securityEvent, $additionalContext);
// Verwende Event-Beschreibung als Message falls nicht gesetzt
$finalMessage = $message ?: $securityEvent->getDescription();
parent::__construct($finalMessage, $code, $previous, $context);
}
/**
* Factory Method für Security-Events
*/
public static function fromEvent(SecurityEventInterface $event, string $message = '', int $code = 0): self
{
return new self($event, $message, $code);
}
/**
* Erstellt Security-spezifischen Context
*/
private function createSecurityContext(
SecurityEventInterface $securityEvent,
?ExceptionContext $additionalContext
): ExceptionContext {
$baseContext = ExceptionContext::forOperation(
'security.' . $securityEvent->getCategory(),
'Security'
)->withData([
'event_type' => $securityEvent->getEventIdentifier(),
'event_category' => $securityEvent->getCategory(),
'event_data' => $securityEvent->toArray(),
'client_ip' => $_SERVER['REMOTE_ADDR'] ?? 'unknown',
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'unknown',
'request_uri' => $_SERVER['REQUEST_URI'] ?? null,
'request_method' => $_SERVER['REQUEST_METHOD'] ?? null,
'timestamp' => time()
])->withMetadata([
'security_event' => $securityEvent->getEventIdentifier(),
'security_level' => $securityEvent->getLogLevel()->value,
'security_description' => $securityEvent->getDescription(),
'requires_alert' => $securityEvent->requiresAlert(),
'event_category' => $securityEvent->getCategory()
]);
// Merge mit zusätzlichem Context falls vorhanden
if ($additionalContext) {
$baseContext = $baseContext
->withData($additionalContext->data)
->withDebug($additionalContext->debug)
->withMetadata($additionalContext->metadata);
}
return $baseContext;
}
/**
* Gibt Security-Event zurück
*/
public function getSecurityEvent(): SecurityEventInterface
{
return $this->securityEvent;
}
/**
* Gibt Security-Level zurück
*/
public function getSecurityLevel(): SecurityLogLevel
{
return $this->securityEvent->getLogLevel();
}
/**
* Prüft ob Alert erforderlich ist
*/
public function requiresAlert(): bool
{
return $this->securityEvent->requiresAlert();
}
}

View File

@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace App\Framework\Exception;
/**
* Security-spezifische Log-Level nach OWASP-Standard
*/
enum SecurityLogLevel: string
{
case DEBUG = 'DEBUG';
case INFO = 'INFO';
case WARN = 'WARN';
case ERROR = 'ERROR';
case FATAL = 'FATAL';
/**
* Konvertiert zu Standard-PSR-Log-Level
*/
public function toPsrLevel(): string
{
return match ($this) {
self::DEBUG => 'debug',
self::INFO => 'info',
self::WARN => 'warning',
self::ERROR => 'error',
self::FATAL => 'critical',
};
}
/**
* Numerischer Wert für Vergleiche
*/
public function getNumericValue(): int
{
return match ($this) {
self::DEBUG => 100,
self::INFO => 200,
self::WARN => 300,
self::ERROR => 400,
self::FATAL => 500,
};
}
}

View File

@@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace App\Framework\Exception;
final readonly class SystemContext
{
public function __construct(
public ?string $memoryUsage = null,
public ?float $executionTime = null,
public ?string $phpVersion = null,
public ?string $frameworkVersion = null,
public array $environment = []
) {}
public static function current(): self
{
return new self(
memoryUsage: self::formatBytes(memory_get_usage(true)),
executionTime: isset($_SERVER['REQUEST_TIME_FLOAT'])
? microtime(true) - $_SERVER['REQUEST_TIME_FLOAT']
: null,
phpVersion: PHP_VERSION,
frameworkVersion: '1.0.0', // Aus Config laden
environment: [
'os' => PHP_OS,
'sapi' => PHP_SAPI,
'timezone' => date_default_timezone_get(),
'memory_limit' => ini_get('memory_limit'),
'max_execution_time' => ini_get('max_execution_time')
]
);
}
public static function empty(): self
{
return new self();
}
public function toArray(): array
{
return [
'memory_usage' => $this->memoryUsage,
'execution_time' => $this->executionTime,
'php_version' => $this->phpVersion,
'framework_version' => $this->frameworkVersion,
'environment' => $this->environment
];
}
private static function formatBytes(int $bytes): string
{
$units = ['B', 'KB', 'MB', 'GB'];
$bytes = max($bytes, 0);
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
$pow = min($pow, count($units) - 1);
$bytes /= (1 << (10 * $pow));
return round($bytes, 2) . ' ' . $units[$pow];
}
}

View File

@@ -0,0 +1,123 @@
<?php
declare(strict_types=1);
namespace App\Framework\Exception;
final class ValidationException extends FrameworkException
{
public readonly ValidationResult $validationResult;
public readonly array $errors;
public readonly string $field;
/**
* @param ValidationResult $validationResult Das Validierungsergebnis mit allen Fehlern
* @param string|null $field Optionaler einzelner Feldname für Rückwärtskompatibilität
*/
public function __construct(
ValidationResult $validationResult,
?string $field = null
) {
$this->validationResult = $validationResult;
// Für Rückwärtskompatibilität: Wenn nur ein Feld angegeben wurde, verwende dessen Fehler
if ($field !== null && $validationResult->getFieldErrors($field)) {
$this->field = $field;
$this->errors = $validationResult->getFieldErrors($field);
} else {
// Andernfalls verwende das erste Feld oder einen Standard
$allErrors = $validationResult->getAll();
$firstField = array_key_first($allErrors);
$this->field = $firstField ?? 'unknown';
$this->errors = $firstField ? $allErrors[$firstField] : [];
}
// Erstelle eine aussagekräftige Fehlernachricht aus allen Fehlern
$message = $this->createErrorMessage();
// Erstelle Exception-Kontext
$context = ExceptionContext::forOperation('validation.validate', 'Validator')
->withData([
'failed_fields' => array_keys($validationResult->getAll()),
'error_count' => count($validationResult->getAllErrorMessages()),
'primary_field' => $this->field,
'validation_errors' => $validationResult->getAll()
]);
parent::__construct(message: $message, context: $context);
}
/**
* Erstellt eine strukturierte Fehlernachricht aus allen Validierungsfehlern
*/
private function createErrorMessage(): string
{
$allErrors = $this->validationResult->getAll();
if (empty($allErrors)) {
return 'Unbekannter Validierungsfehler.';
}
$messages = [];
foreach ($allErrors as $field => $fieldErrors) {
$fieldMessage = $field . ': ' . implode(', ', $fieldErrors);
$messages[] = $fieldMessage;
}
return implode('; ', $messages);
}
/**
* Gibt alle Fehlermeldungen für ein bestimmtes Feld zurück
*
* @param string $field Feldname
* @return array<string> Liste der Fehlermeldungen für das Feld
*/
public function getFieldErrors(string $field): array
{
return $this->validationResult->getFieldErrors($field);
}
/**
* Gibt alle Fehlermeldungen als Array zurück
*
* @return array<string, string[]> Alle Fehlermeldungen gruppiert nach Feldern
*/
public function getAllErrors(): array
{
return $this->validationResult->getAll();
}
/**
* Gibt alle Fehlermeldungen als flache Liste zurück
*
* @return array<string> Liste aller Fehlermeldungen
*/
public function getAllErrorMessages(): array
{
return $this->validationResult->getAllErrorMessages();
}
/**
* Prüft, ob ein bestimmtes Feld Fehler hat
*/
public function hasFieldErrors(string $field): bool
{
return !empty($this->validationResult->getFieldErrors($field));
}
/**
* Statische Factory-Methode für einfache Einzelfeld-Fehler (Rückwärtskompatibilität)
*
* @param array<string> $errors Liste der Fehlermeldungen
* @param string $field Feldname
* @return self
*/
public static function forField(array $errors, string $field): self
{
$validationResult = new ValidationResult();
$validationResult->addErrors($field, $errors);
return new self($validationResult, $field);
}
}