feat: CI/CD pipeline setup complete - Ansible playbooks updated, secrets configured, workflow ready

This commit is contained in:
2025-10-31 01:39:24 +01:00
parent 55c04e4fd0
commit e26eb2aa12
601 changed files with 44184 additions and 32477 deletions

View File

@@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
namespace App\Framework\Http;
/**
* Value Object for custom/unknown MIME types not in the MimeType enum
*/
final readonly class CustomMimeType implements MimeTypeInterface
{
private string $primaryType;
private string $subType;
public function __construct(
private string $value
) {
$this->validate($value);
[$this->primaryType, $this->subType] = $this->parse($value);
}
public static function fromString(string $mimeType): self
{
return new self($mimeType);
}
public function getValue(): string
{
return $this->value;
}
public function getPrimaryType(): string
{
return $this->primaryType;
}
public function getSubType(): string
{
return $this->subType;
}
public function equals(MimeTypeInterface $other): bool
{
return $this->value === $other->getValue();
}
public function __toString(): string
{
return $this->value;
}
private function validate(string $value): void
{
if (empty($value)) {
throw new \InvalidArgumentException('MIME type cannot be empty');
}
if (!str_contains($value, '/')) {
throw new \InvalidArgumentException("Invalid MIME type format: {$value}");
}
}
/**
* @return array{string, string}
*/
private function parse(string $value): array
{
$parts = explode('/', $value, 2);
return [$parts[0], $parts[1]];
}
}

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace App\Framework\Http;
use App\Framework\Filesystem\ValueObjects\FilePath;
/**
* Production MIME type detector using PHP's mime_content_type()
*
* Detects MIME types from actual file content, returns MimeType enum if known
* or CustomMimeType for unknown types
*/
final readonly class FilesystemMimeTypeDetector implements MimeTypeDetector
{
public function detect(FilePath $filePath): MimeTypeInterface
{
if (!$filePath->exists()) {
throw new \RuntimeException("File does not exist: {$filePath}");
}
$mimeTypeString = mime_content_type($filePath->toString());
if ($mimeTypeString === false) {
throw new \RuntimeException("Could not detect MIME type for: {$filePath}");
}
// Try to map to known MimeType enum
$mimeType = MimeType::tryFromString($mimeTypeString);
// If not found in enum, create CustomMimeType
return $mimeType ?? CustomMimeType::fromString($mimeTypeString);
}
}

View File

@@ -4,7 +4,6 @@ declare(strict_types=1);
namespace App\Framework\Http\Middlewares;
use App\Framework\ErrorHandling\SecurityEventLogger;
use App\Framework\Exception\SecurityEvent\SystemExcessiveUseEvent;
use App\Framework\Http\Headers;
use App\Framework\Http\HttpMiddleware;
@@ -17,6 +16,10 @@ use App\Framework\Http\RequestStateManager;
use App\Framework\Http\Response;
use App\Framework\Http\ResponseManipulator;
use App\Framework\Http\Status;
use App\Framework\Logging\Logger;
use App\Framework\Logging\Processors\SecurityEventProcessor;
use App\Framework\Logging\ValueObjects\LogContext;
use App\Framework\Logging\ValueObjects\SecurityContext;
use App\Framework\RateLimit\RateLimitConfig;
use App\Framework\RateLimit\RateLimiter;
use App\Framework\RateLimit\RateLimitResult;
@@ -33,8 +36,9 @@ final readonly class RateLimitMiddleware implements HttpMiddleware
public function __construct(
private RateLimiter $rateLimiter,
private ResponseManipulator $responseManipulator,
private RateLimitConfig $config = new RateLimitConfig(),
private ?SecurityEventLogger $securityLogger = null
private readonly Logger $logger,
private readonly SecurityEventProcessor $processor,
private RateLimitConfig $config = new RateLimitConfig()
) {
}
@@ -146,16 +150,30 @@ final readonly class RateLimitMiddleware implements HttpMiddleware
private function logSecurityEvent(string $clientIp, string $path, RateLimitResult $result): void
{
if (! $this->securityLogger) {
return;
}
// Create security event with correct constructor parameters
$event = new SystemExcessiveUseEvent(
null, // No user ID for IP-based limiting
$clientIp,
"Rate limit exceeded for {$path}: {$result->getCurrent()}/{$result->getLimit()} requests"
$result->getLimit(),
$result->getCurrent()
);
$this->securityLogger->log($event);
// Create SecurityContext for OWASP-compliant logging
$securityContext = SecurityContext::forIntrusion(
eventId: $event->getEventIdentifier(),
description: $event->getDescription(),
level: $event->getLogLevel(),
requiresAlert: $event->requiresAlert(),
eventData: array_merge($event->toArray(), ['path' => $path])
)->withRequestInfo($clientIp, null);
// Map SecurityLogLevel to framework LogLevel
$logLevel = $this->processor->mapSecurityLevelToLogLevel($event->getLogLevel());
// Log directly via Logger with SecurityContext
$this->logger->log(
$logLevel,
$event->getDescription(),
LogContext::empty()->withSecurityContext($securityContext)
);
}
}

View File

@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace App\Framework\Http;
enum MimeType: string
enum MimeType: string implements MimeTypeInterface
{
// Text
case TEXT_PLAIN = 'text/plain';
@@ -68,6 +68,27 @@ enum MimeType: string
case FONT_TTF = 'font/ttf';
case FONT_OTF = 'font/otf';
// MimeTypeInterface implementation
public function getValue(): string
{
return $this->value;
}
public function getPrimaryType(): string
{
return explode('/', $this->value)[0];
}
public function getSubType(): string
{
return explode('/', $this->value)[1];
}
public function equals(MimeTypeInterface $other): bool
{
return $this->value === $other->getValue();
}
// Convenience methods using analyzer classes
public function isImage(): bool
{
@@ -119,17 +140,17 @@ enum MimeType: string
return MimeTypeResolver::getPreferredExtension($this);
}
public static function fromExtension(string $extension): ?self
public static function tryFromExtension(string $extension): ?self
{
return MimeTypeResolver::fromExtension($extension);
}
public static function fromFilePath(string $filePath): ?self
public static function tryFromFilePath(string $filePath): ?self
{
return MimeTypeResolver::fromFilePath($filePath);
}
public static function fromString(string $mimeTypeString): ?self
public static function tryFromString(string $mimeTypeString): ?self
{
return MimeTypeResolver::fromString($mimeTypeString);
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace App\Framework\Http;
use App\Framework\Filesystem\ValueObjects\FilePath;
/**
* Interface for detecting MIME types from file paths
*
* Returns MimeTypeInterface which can be either:
* - MimeType enum for known types
* - CustomMimeType for unknown types
*/
interface MimeTypeDetector
{
/**
* Detect MIME type from file path
*
* @return MimeTypeInterface The detected MIME type
*/
public function detect(FilePath $filePath): MimeTypeInterface;
}

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace App\Framework\Http;
/**
* Interface for MIME type representations
*
* Implemented by both MimeType enum (for known types) and CustomMimeType (for unknown types)
*/
interface MimeTypeInterface
{
/**
* Get the MIME type string value (e.g., 'image/jpeg', 'text/plain')
*/
public function getValue(): string;
/**
* Get the primary type (e.g., 'image', 'text', 'application')
*/
public function getPrimaryType(): string;
/**
* Get the sub type (e.g., 'jpeg', 'plain', 'json')
*/
public function getSubType(): string;
/**
* Check if this MIME type equals another
*/
public function equals(self $other): bool;
}

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace App\Framework\Http;
use App\Framework\Filesystem\ValueObjects\FilePath;
/**
* Mock MIME type detector for testing
*
* Returns a predefined MIME type instead of detecting from file content
*/
final readonly class MockMimeTypeDetector implements MimeTypeDetector
{
public function __construct(
private MimeTypeInterface $mimeType
) {
}
/**
* Create from MIME type string
*/
public static function fromString(string $mimeTypeString): self
{
$mimeType = MimeType::tryFromString($mimeTypeString)
?? CustomMimeType::fromString($mimeTypeString);
return new self($mimeType);
}
/**
* Always returns the predefined MIME type, ignoring the file path
*/
public function detect(FilePath $filePath): MimeTypeInterface
{
return $this->mimeType;
}
}

View File

@@ -0,0 +1,105 @@
<?php
declare(strict_types=1);
namespace App\Framework\Http;
/**
* Request Context Value Object
*
* Encapsulates request-scoped context information for tracking and auditing.
* Used by StateHistoryManager for capturing request metadata.
*
* Immutable value object following framework patterns.
*/
final readonly class RequestContext
{
public function __construct(
private ?string $userId = null,
private ?string $sessionId = null,
private ?string $ipAddress = null,
private ?string $userAgent = null,
) {}
/**
* Create RequestContext from HttpRequest
*/
public static function fromRequest(HttpRequest $request): self
{
return new self(
userId: null, // Must be set separately after authentication
sessionId: $request->session?->getId(),
ipAddress: (string) $request->server->getRemoteAddr(),
userAgent: $request->server->getUserAgent()?->toString(),
);
}
/**
* Create new instance with userId
*/
public function withUserId(string $userId): self
{
return new self(
userId: $userId,
sessionId: $this->sessionId,
ipAddress: $this->ipAddress,
userAgent: $this->userAgent,
);
}
public function getUserId(): ?string
{
return $this->userId;
}
public function getSessionId(): ?string
{
return $this->sessionId;
}
public function getIpAddress(): ?string
{
return $this->ipAddress;
}
public function getUserAgent(): ?string
{
return $this->userAgent;
}
/**
* Check if context has user authentication
*/
public function isAuthenticated(): bool
{
return $this->userId !== null;
}
/**
* Convert to array for logging/serialization
*
* @return array{user_id?: string, session_id?: string, ip_address?: string, user_agent?: string}
*/
public function toArray(): array
{
$data = [];
if ($this->userId !== null) {
$data['user_id'] = $this->userId;
}
if ($this->sessionId !== null) {
$data['session_id'] = $this->sessionId;
}
if ($this->ipAddress !== null) {
$data['ip_address'] = $this->ipAddress;
}
if ($this->userAgent !== null) {
$data['user_agent'] = $this->userAgent;
}
return $data;
}
}

View File

@@ -46,7 +46,7 @@ final class RequestIdGenerator
$headerRequestId = $headers?->getFirst(self::REQUEST_ID_HEADER);
// Neue RequestId erstellen (validiert automatisch die Header-ID, falls vorhanden)
$this->requestId = new RequestId($headerRequestId, $this->secret);
$this->requestId = new RequestId($this->secret, $headerRequestId);
}
return $this->requestId;

View File

@@ -4,6 +4,8 @@ declare(strict_types=1);
namespace App\Framework\Http;
use App\Framework\Filesystem\ValueObjects\FilePath;
final readonly class UploadedFile
{
public function __construct(
@@ -12,7 +14,8 @@ final readonly class UploadedFile
public int $size,
public string $tmpName,
public UploadError $error,
private bool $skipValidation = false
private bool $skipValidation = false,
private ?MimeTypeDetector $mimeTypeDetector = null
) {
if (! $this->skipValidation && $this->isValid() === false) {
throw new \InvalidArgumentException("Invalid uploaded file: {$this->name}");
@@ -34,13 +37,16 @@ final readonly class UploadedFile
return $this->error === UploadError::OK && is_uploaded_file($this->tmpName);
}
public function getMimeType(): string
public function getMimeType(): MimeTypeInterface
{
return mime_content_type($this->tmpName);
$detector = $this->mimeTypeDetector ?? new FilesystemMimeTypeDetector();
return $detector->detect(FilePath::create($this->tmpName));
}
/**
* Create an UploadedFile for testing without validation
*
* Uses MockMimeTypeDetector to return the specified type instead of detecting from file
*/
public static function createForTesting(
string $name,
@@ -49,6 +55,14 @@ final readonly class UploadedFile
string $tmpName,
UploadError $error = UploadError::OK
): self {
return new self($name, $type, $size, $tmpName, $error, skipValidation: true);
return new self(
$name,
$type,
$size,
$tmpName,
$error,
skipValidation: true,
mimeTypeDetector: MockMimeTypeDetector::fromString($type)
);
}
}