Enable Discovery debug logging for production troubleshooting
- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Config;
|
||||
|
||||
use App\Framework\DateTime\Timezone;
|
||||
@@ -11,8 +13,34 @@ final readonly class AppConfig
|
||||
public string $version = '1.0.0',
|
||||
public string $environment = 'production',
|
||||
public bool $debug = false,
|
||||
public Timezone $timezone = Timezone::DEFAULT,
|
||||
public Timezone $timezone = Timezone::EuropeBerlin,
|
||||
public string $locale = 'en',
|
||||
public EnvironmentType $type = EnvironmentType::DEV
|
||||
) {}
|
||||
) {
|
||||
}
|
||||
|
||||
public function isProduction(): bool
|
||||
{
|
||||
return $this->type->isProduction();
|
||||
}
|
||||
|
||||
public function isDevelopment(): bool
|
||||
{
|
||||
return $this->type->isDevelopment();
|
||||
}
|
||||
|
||||
public function isStaging(): bool
|
||||
{
|
||||
return $this->type->isStaging();
|
||||
}
|
||||
|
||||
public function isProductionLike(): bool
|
||||
{
|
||||
return $this->type->isProductionLike();
|
||||
}
|
||||
|
||||
public function isDebugEnabled(): bool
|
||||
{
|
||||
return $this->debug || $this->type->isDebugEnabled();
|
||||
}
|
||||
}
|
||||
|
||||
122
src/Framework/Config/ConfigValidator.php
Normal file
122
src/Framework/Config/ConfigValidator.php
Normal file
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Config;
|
||||
|
||||
/**
|
||||
* Lightweight boot-time configuration validator.
|
||||
*
|
||||
* Non-invasive: reports issues via error_log and returns a list of problems
|
||||
* without throwing, so existing environments are not broken inadvertently.
|
||||
*/
|
||||
final readonly class ConfigValidator
|
||||
{
|
||||
public function __construct(private Environment $env)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate selected environment variables and return a list of issues.
|
||||
* Each issue contains: key, issue, severity, recommendation.
|
||||
*/
|
||||
public function validate(): array
|
||||
{
|
||||
$issues = [];
|
||||
|
||||
// APP_ENV validation
|
||||
$allowedEnvs = ['development', 'testing', 'production'];
|
||||
$appEnv = $this->env->getString(EnvKey::APP_ENV, 'production');
|
||||
if (!in_array($appEnv, $allowedEnvs, true)) {
|
||||
$issues[] = [
|
||||
'key' => 'APP_ENV',
|
||||
'issue' => 'invalid_value',
|
||||
'severity' => 'medium',
|
||||
'recommendation' => 'Setze APP_ENV auf einen der Werte: development | testing | production',
|
||||
];
|
||||
}
|
||||
|
||||
// APP_DEBUG should be boolean-like if present
|
||||
if ($this->env->has(EnvKey::APP_DEBUG)) {
|
||||
$raw = $this->env->get(EnvKey::APP_DEBUG);
|
||||
if (!is_bool($raw) && !is_string($raw)) {
|
||||
$issues[] = [
|
||||
'key' => 'APP_DEBUG',
|
||||
'issue' => 'invalid_type',
|
||||
'severity' => 'low',
|
||||
'recommendation' => 'APP_DEBUG sollte true/false (oder "true"/"false") sein.',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// APP_PORT if present should be a valid TCP port
|
||||
if ($this->env->has('APP_PORT')) {
|
||||
$port = (int) $this->env->get('APP_PORT');
|
||||
if ($port < 1 || $port > 65535) {
|
||||
$issues[] = [
|
||||
'key' => 'APP_PORT',
|
||||
'issue' => 'out_of_range',
|
||||
'severity' => 'medium',
|
||||
'recommendation' => 'APP_PORT muss zwischen 1 und 65535 liegen.',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Redis port if present
|
||||
if ($this->env->has('REDIS_PORT')) {
|
||||
$redisPort = (int) $this->env->get('REDIS_PORT');
|
||||
if ($redisPort < 1 || $redisPort > 65535) {
|
||||
$issues[] = [
|
||||
'key' => 'REDIS_PORT',
|
||||
'issue' => 'out_of_range',
|
||||
'severity' => 'low',
|
||||
'recommendation' => 'REDIS_PORT muss zwischen 1 und 65535 liegen.',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Rate limit values if present: must be non-negative
|
||||
foreach ([
|
||||
'RATE_LIMIT_DEFAULT',
|
||||
'RATE_LIMIT_WINDOW',
|
||||
'RATE_LIMIT_AUTH',
|
||||
'RATE_LIMIT_AUTH_WINDOW',
|
||||
'RATE_LIMIT_API',
|
||||
'RATE_LIMIT_API_WINDOW',
|
||||
] as $rateKey) {
|
||||
if ($this->env->has($rateKey)) {
|
||||
$value = (int) $this->env->get($rateKey);
|
||||
if ($value < 0) {
|
||||
$issues[] = [
|
||||
'key' => $rateKey,
|
||||
'issue' => 'negative_value',
|
||||
'severity' => 'low',
|
||||
'recommendation' => $rateKey . ' sollte eine nicht-negative Ganzzahl sein.',
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $issues;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate and log issues. Returns the list of issues for optional handling.
|
||||
*/
|
||||
public function validateAndReport(): array
|
||||
{
|
||||
$issues = $this->validate();
|
||||
foreach ($issues as $issue) {
|
||||
$msg = sprintf(
|
||||
'[CONFIG] key=%s issue=%s severity=%s recommendation=%s',
|
||||
$issue['key'],
|
||||
$issue['issue'],
|
||||
$issue['severity'],
|
||||
$issue['recommendation']
|
||||
);
|
||||
error_log($msg);
|
||||
}
|
||||
|
||||
return $issues;
|
||||
}
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Config;
|
||||
|
||||
class Configuration
|
||||
{
|
||||
private array $config = [];
|
||||
|
||||
/**
|
||||
* Lädt Konfigurationen aus einer oder mehreren Quellen
|
||||
*/
|
||||
public function __construct(array $config = [])
|
||||
{
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt Konfigurationen aus PHP-Dateien
|
||||
*/
|
||||
public function loadFromFile(string $path): self
|
||||
{
|
||||
if (file_exists($path) && is_readable($path)) {
|
||||
$fileConfig = require $path;
|
||||
if (is_array($fileConfig)) {
|
||||
$this->config = array_merge($this->config, $fileConfig);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt Konfigurationen aus einem Verzeichnis
|
||||
*/
|
||||
public function loadFromDirectory(string $directory): self
|
||||
{
|
||||
if (!is_dir($directory)) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
foreach (glob($directory . '/*.php') as $file) {
|
||||
$this->loadFromFile($file);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt einen Konfigurationswert zurück
|
||||
*/
|
||||
public function get(string $key, mixed $default = null): mixed
|
||||
{
|
||||
$keys = explode('.', $key);
|
||||
$value = $this->config;
|
||||
|
||||
foreach ($keys as $segment) {
|
||||
if (!is_array($value) || !array_key_exists($segment, $value)) {
|
||||
return $default;
|
||||
}
|
||||
$value = $value[$segment];
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt einen Konfigurationswert
|
||||
*/
|
||||
public function set(string $key, mixed $value): self
|
||||
{
|
||||
$keys = explode('.', $key);
|
||||
$config = &$this->config;
|
||||
|
||||
foreach ($keys as $i => $segment) {
|
||||
if ($i === count($keys) - 1) {
|
||||
$config[$segment] = $value;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!isset($config[$segment]) || !is_array($config[$segment])) {
|
||||
$config[$segment] = [];
|
||||
}
|
||||
|
||||
$config = &$config[$segment];
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft, ob ein Konfigurationswert existiert
|
||||
*/
|
||||
public function has(string $key): bool
|
||||
{
|
||||
$keys = explode('.', $key);
|
||||
$config = $this->config;
|
||||
|
||||
foreach ($keys as $segment) {
|
||||
if (!is_array($config) || !array_key_exists($segment, $config)) {
|
||||
return false;
|
||||
}
|
||||
$config = $config[$segment];
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt alle Konfigurationswerte zurück
|
||||
*/
|
||||
public function all(): array
|
||||
{
|
||||
return $this->config;
|
||||
}
|
||||
}
|
||||
270
src/Framework/Config/Console/SecretsCommand.php
Normal file
270
src/Framework/Config/Console/SecretsCommand.php
Normal file
@@ -0,0 +1,270 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Config\Console;
|
||||
|
||||
use App\Framework\Config\EncryptedEnvLoader;
|
||||
use App\Framework\Config\Environment;
|
||||
use App\Framework\Config\SecretManager;
|
||||
use App\Framework\Console\ConsoleColor;
|
||||
use App\Framework\Console\ConsoleCommand;
|
||||
use App\Framework\Console\ConsoleInput;
|
||||
use App\Framework\Console\ConsoleOutput;
|
||||
use App\Framework\Console\ExitCode;
|
||||
use App\Framework\Encryption\EncryptionFactory;
|
||||
use App\Framework\Random\RandomGenerator;
|
||||
|
||||
/**
|
||||
* Console commands for secrets management
|
||||
*/
|
||||
final readonly class SecretsCommand
|
||||
{
|
||||
public function __construct(
|
||||
private RandomGenerator $randomGenerator,
|
||||
private EncryptionFactory $encryptionFactory,
|
||||
private ?SecretManager $secretManager = null
|
||||
) {
|
||||
}
|
||||
|
||||
#[ConsoleCommand('secrets:generate-key', 'Generate a secure encryption key')]
|
||||
public function generateKey(ConsoleInput $input, ConsoleOutput $output): ExitCode
|
||||
{
|
||||
$output->writeLine('🔐 Generating encryption key...', ConsoleColor::CYAN);
|
||||
$output->newLine();
|
||||
|
||||
try {
|
||||
$length = 32;
|
||||
$keyBytes = $this->randomGenerator->bytes($length);
|
||||
$encodedKey = base64_encode($keyBytes);
|
||||
|
||||
$availableMethods = $this->encryptionFactory->getAvailableMethods();
|
||||
$recommendedMethod = $this->encryptionFactory->getRecommendedMethod();
|
||||
|
||||
$output->writeLine("Available methods: " . implode(', ', $availableMethods));
|
||||
$output->writeLine("Using recommended method: {$recommendedMethod}", ConsoleColor::GREEN);
|
||||
$output->newLine();
|
||||
|
||||
$output->writeSuccess('✅ Encryption key generated successfully!');
|
||||
$output->newLine();
|
||||
$output->writeLine('ENCRYPTION_KEY=' . $encodedKey, ConsoleColor::YELLOW);
|
||||
$output->newLine();
|
||||
|
||||
$output->writeWarning('🚨 SECURITY WARNINGS:');
|
||||
$output->writeLine('• Store this key securely - it cannot be recovered if lost');
|
||||
$output->writeLine('• Add this to your .env file (never commit to version control)');
|
||||
$output->writeLine('• Use different keys for different environments');
|
||||
$output->newLine();
|
||||
|
||||
$output->writeInfo('📋 Next steps:');
|
||||
$output->writeLine('1. Add the key to your .env file');
|
||||
$output->writeLine('2. Create .env.secrets file: php console.php secrets:init');
|
||||
$output->writeLine('3. Encrypt secrets: php console.php secrets:encrypt "secret-value"');
|
||||
|
||||
return ExitCode::SUCCESS;
|
||||
} catch (\Throwable $e) {
|
||||
$output->writeError('❌ Failed to generate encryption key: ' . $e->getMessage());
|
||||
|
||||
return ExitCode::GENERAL_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
#[ConsoleCommand('secrets:encrypt', 'Encrypt a secret value')]
|
||||
public function encrypt(ConsoleInput $input, ConsoleOutput $output): ExitCode
|
||||
{
|
||||
if ($this->secretManager === null) {
|
||||
$output->writeError('❌ SecretManager not available. Make sure ENCRYPTION_KEY is set in .env');
|
||||
|
||||
return ExitCode::CONFIG_ERROR;
|
||||
}
|
||||
|
||||
$value = $input->getArgument(0);
|
||||
if (! $value) {
|
||||
$value = $output->askPassword('Enter secret value to encrypt:');
|
||||
}
|
||||
|
||||
if (empty($value)) {
|
||||
$output->writeError('❌ No value provided to encrypt');
|
||||
|
||||
return ExitCode::CONFIG_ERROR;
|
||||
}
|
||||
|
||||
try {
|
||||
$encrypted = $this->secretManager->encryptSecret($value);
|
||||
|
||||
$output->writeSuccess('✅ Value encrypted successfully!');
|
||||
$output->newLine();
|
||||
$output->writeLine('Encrypted value:', ConsoleColor::CYAN);
|
||||
$output->writeLine($encrypted, ConsoleColor::YELLOW);
|
||||
$output->newLine();
|
||||
$output->writeInfo('💡 Add this to your .env.secrets file like:');
|
||||
$output->writeLine('SECRET_YOUR_KEY=' . $encrypted, ConsoleColor::GRAY);
|
||||
|
||||
return ExitCode::SUCCESS;
|
||||
} catch (\Throwable $e) {
|
||||
$output->writeError('❌ Failed to encrypt value: ' . $e->getMessage());
|
||||
|
||||
return ExitCode::SOFTWARE_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
#[ConsoleCommand('secrets:decrypt', 'Decrypt a secret value (for debugging)')]
|
||||
public function decrypt(ConsoleInput $input, ConsoleOutput $output): ExitCode
|
||||
{
|
||||
if ($this->secretManager === null) {
|
||||
$output->writeError('❌ SecretManager not available. Make sure ENCRYPTION_KEY is set in .env');
|
||||
|
||||
return ExitCode::CONFIG_ERROR;
|
||||
}
|
||||
|
||||
$encryptedValue = $input->getArgument(0);
|
||||
if (! $encryptedValue) {
|
||||
$encryptedValue = $output->askQuestion('Enter encrypted value to decrypt:');
|
||||
}
|
||||
|
||||
if (empty($encryptedValue)) {
|
||||
$output->writeError('❌ No encrypted value provided');
|
||||
|
||||
return ExitCode::INVALID_INPUT;
|
||||
}
|
||||
|
||||
try {
|
||||
if (! $this->secretManager->isEncrypted($encryptedValue)) {
|
||||
$output->writeWarning('⚠️ Value does not appear to be encrypted');
|
||||
|
||||
return ExitCode::INVALID_INPUT;
|
||||
}
|
||||
|
||||
$decrypted = $this->secretManager->getSecret('TEMP_DECRYPT', $encryptedValue);
|
||||
|
||||
$output->writeSuccess('✅ Value decrypted successfully!');
|
||||
$output->newLine();
|
||||
$output->writeLine('Decrypted value:', ConsoleColor::CYAN);
|
||||
$output->writeLine($decrypted, ConsoleColor::YELLOW);
|
||||
$output->newLine();
|
||||
$output->writeWarning('🔒 This value should be kept secure!');
|
||||
|
||||
return ExitCode::SUCCESS;
|
||||
} catch (\Throwable $e) {
|
||||
$output->writeError('❌ Failed to decrypt value: ' . $e->getMessage());
|
||||
|
||||
return ExitCode::SOFTWARE_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
#[ConsoleCommand('secrets:init', 'Initialize .env.secrets file')]
|
||||
public function init(ConsoleInput $input, ConsoleOutput $output): ExitCode
|
||||
{
|
||||
$basePath = getcwd();
|
||||
$secretsFile = $basePath . '/.env.secrets';
|
||||
|
||||
if (file_exists($secretsFile)) {
|
||||
if (! $output->confirm('⚠️ .env.secrets already exists. Overwrite?', false)) {
|
||||
$output->writeInfo('Operation cancelled');
|
||||
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$encryptedLoader = new EncryptedEnvLoader($this->encryptionFactory, $this->randomGenerator);
|
||||
$filePath = $encryptedLoader->generateSecretsTemplate($basePath);
|
||||
|
||||
$output->writeSuccess('✅ .env.secrets template created!');
|
||||
$output->writeLine("File created at: {$filePath}", ConsoleColor::GRAY);
|
||||
$output->newLine();
|
||||
$output->writeInfo('📋 Next steps:');
|
||||
$output->writeLine('1. Edit .env.secrets and add your secret keys');
|
||||
$output->writeLine('2. Encrypt values: php console.php secrets:encrypt "your-value"');
|
||||
$output->writeLine('3. Add encrypted values to .env.secrets');
|
||||
|
||||
return ExitCode::SUCCESS;
|
||||
} catch (\Throwable $e) {
|
||||
$output->writeError('❌ Failed to create .env.secrets template: ' . $e->getMessage());
|
||||
|
||||
return ExitCode::CANT_CREATE;
|
||||
}
|
||||
}
|
||||
|
||||
#[ConsoleCommand('secrets:list', 'List all secret keys')]
|
||||
public function list(ConsoleInput $input, ConsoleOutput $output): ExitCode
|
||||
{
|
||||
if ($this->secretManager === null) {
|
||||
$output->writeError('❌ SecretManager not available. Make sure ENCRYPTION_KEY is set in .env');
|
||||
|
||||
return ExitCode::CONFIG_ERROR;
|
||||
}
|
||||
|
||||
try {
|
||||
$secretKeys = $this->secretManager->getSecretKeys();
|
||||
|
||||
if (empty($secretKeys)) {
|
||||
$output->writeInfo('ℹ️ No secret keys found');
|
||||
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
|
||||
$output->writeLine('🔐 Found secret keys:', ConsoleColor::CYAN);
|
||||
$output->newLine();
|
||||
|
||||
foreach ($secretKeys as $key) {
|
||||
$output->writeLine("• {$key}", ConsoleColor::WHITE);
|
||||
}
|
||||
|
||||
$output->newLine();
|
||||
$output->writeLine("Total: " . count($secretKeys) . " secrets", ConsoleColor::GRAY);
|
||||
|
||||
return ExitCode::SUCCESS;
|
||||
} catch (\Throwable $e) {
|
||||
$output->writeError('❌ Failed to list secrets: ' . $e->getMessage());
|
||||
|
||||
return ExitCode::CONFIG_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
#[ConsoleCommand('secrets:validate', 'Validate secrets setup')]
|
||||
public function validate(ConsoleInput $input, ConsoleOutput $output): ExitCode
|
||||
{
|
||||
$basePath = getcwd();
|
||||
$encryptedLoader = new EncryptedEnvLoader($this->encryptionFactory, $this->randomGenerator);
|
||||
$env = Environment::fromFile($basePath . '/.env');
|
||||
$encryptionKey = $env->get('ENCRYPTION_KEY');
|
||||
|
||||
$output->writeLine('🔍 Validating secrets setup...', ConsoleColor::CYAN);
|
||||
$output->newLine();
|
||||
|
||||
$issues = $encryptedLoader->validateEncryptionSetup($basePath, $encryptionKey);
|
||||
|
||||
if (empty($issues)) {
|
||||
$output->writeSuccess('✅ Secrets setup is valid!');
|
||||
|
||||
if ($this->secretManager !== null) {
|
||||
$context = $this->secretManager->getSecurityContext();
|
||||
$output->newLine();
|
||||
$output->writeInfo('Security context:');
|
||||
$output->writeLine("• HTTPS: " . ($context->isHttps ? 'Yes' : 'No'));
|
||||
$output->writeLine("• Encryption: {$context->encryptionMethod}");
|
||||
$output->writeLine("• Server: {$context->serverName}");
|
||||
$output->writeLine("• Risk Level: {$context->getRiskLevel()->getDisplayName()}");
|
||||
$output->writeLine("• Summary: {$context->getSummary()}");
|
||||
}
|
||||
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
|
||||
$output->writeWarning('⚠️ Found issues with secrets setup:');
|
||||
$output->newLine();
|
||||
|
||||
foreach ($issues as $issue) {
|
||||
$color = match ($issue['severity']) {
|
||||
'high' => ConsoleColor::RED,
|
||||
'medium' => ConsoleColor::YELLOW,
|
||||
default => ConsoleColor::WHITE
|
||||
};
|
||||
|
||||
$output->writeLine("• {$issue['message']}", $color);
|
||||
}
|
||||
|
||||
return ExitCode::CONFIG_ERROR;
|
||||
}
|
||||
}
|
||||
263
src/Framework/Config/DiscoveryConfig.php
Normal file
263
src/Framework/Config/DiscoveryConfig.php
Normal file
@@ -0,0 +1,263 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Config;
|
||||
|
||||
use App\Framework\Core\AttributeMapper;
|
||||
|
||||
final readonly class DiscoveryConfig
|
||||
{
|
||||
/**
|
||||
* Konstanten für Standardwerte
|
||||
*/
|
||||
public const int DEFAULT_CHUNK_SIZE = 100;
|
||||
public const int DEFAULT_MAX_RETRIES = 3;
|
||||
public const float DEFAULT_MEMORY_THRESHOLD_HIGH = 0.8;
|
||||
public const float DEFAULT_MEMORY_THRESHOLD_CRITICAL = 0.9;
|
||||
public const int DEFAULT_WORKER_COUNT = 4;
|
||||
public const int DEFAULT_CACHE_TTL = 3600; // 1 hour
|
||||
|
||||
/**
|
||||
* @param array<AttributeMapper> $attributeMappers
|
||||
* @param array<class-string> $targetInterfaces
|
||||
*/
|
||||
public function __construct(
|
||||
// Basis-Konfiguration
|
||||
public bool $useCache = true,
|
||||
public bool $showProgress = false,
|
||||
public bool $useContextAwareInitializers = false,
|
||||
public bool $enableCircuitBreaker = true,
|
||||
public array $attributeMappers = [],
|
||||
public array $targetInterfaces = [],
|
||||
|
||||
// Memory-Management-Konfiguration
|
||||
public bool $enableAdaptiveChunking = true,
|
||||
public int $chunkSize = self::DEFAULT_CHUNK_SIZE,
|
||||
public float $memoryThresholdHigh = self::DEFAULT_MEMORY_THRESHOLD_HIGH,
|
||||
public float $memoryThresholdCritical = self::DEFAULT_MEMORY_THRESHOLD_CRITICAL,
|
||||
|
||||
// Asynchrone Verarbeitung
|
||||
public bool $enableAsyncProcessing = false,
|
||||
public ?int $workerCount = null, // null = automatisch bestimmen
|
||||
public bool $enableWorkloadBalancing = true,
|
||||
|
||||
// Fehlerbehandlung
|
||||
public int $maxRetries = self::DEFAULT_MAX_RETRIES,
|
||||
public string $logLevel = 'info', // 'debug', 'info', 'warning', 'error'
|
||||
public bool $continueOnError = true,
|
||||
|
||||
// Cache-Validierung
|
||||
public int $cacheTtl = self::DEFAULT_CACHE_TTL,
|
||||
public bool $enableChecksumValidation = true,
|
||||
public bool $enableCacheWarming = false,
|
||||
) {
|
||||
// Validiere Werte
|
||||
$this->validateConfig();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validiert die Konfigurationswerte
|
||||
* @throws \InvalidArgumentException wenn ungültige Werte gefunden werden
|
||||
*/
|
||||
private function validateConfig(): void
|
||||
{
|
||||
// Validiere numerische Werte
|
||||
if ($this->chunkSize < 1) {
|
||||
throw new \InvalidArgumentException('Chunk size must be at least 1');
|
||||
}
|
||||
|
||||
if ($this->memoryThresholdHigh <= 0 || $this->memoryThresholdHigh >= 1) {
|
||||
throw new \InvalidArgumentException('Memory threshold high must be between 0 and 1');
|
||||
}
|
||||
|
||||
if ($this->memoryThresholdCritical <= $this->memoryThresholdHigh || $this->memoryThresholdCritical >= 1) {
|
||||
throw new \InvalidArgumentException('Memory threshold critical must be between memory threshold high and 1');
|
||||
}
|
||||
|
||||
if ($this->workerCount !== null && $this->workerCount < 1) {
|
||||
throw new \InvalidArgumentException('Worker count must be at least 1');
|
||||
}
|
||||
|
||||
if ($this->maxRetries < 0) {
|
||||
throw new \InvalidArgumentException('Max retries must be at least 0');
|
||||
}
|
||||
|
||||
if ($this->cacheTtl < 0) {
|
||||
throw new \InvalidArgumentException('Cache TTL must be at least 0');
|
||||
}
|
||||
|
||||
// Validiere Enum-Werte
|
||||
$validLogLevels = ['debug', 'info', 'warning', 'error'];
|
||||
if (! in_array($this->logLevel, $validLogLevels, true)) {
|
||||
throw new \InvalidArgumentException('Log level must be one of: ' . implode(', ', $validLogLevels));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt Konfiguration aus Umgebungsvariablen
|
||||
*/
|
||||
public static function fromEnvironment(Environment $env): self
|
||||
{
|
||||
return new self(
|
||||
// Basis-Konfiguration
|
||||
useCache: $env->getBool('DISCOVERY_USE_CACHE', true),
|
||||
showProgress: $env->getBool('DISCOVERY_SHOW_PROGRESS', false),
|
||||
useContextAwareInitializers: $env->getBool('ENABLE_CONTEXT_AWARE_INITIALIZERS', false),
|
||||
enableCircuitBreaker: $env->getBool('DISCOVERY_ENABLE_CIRCUIT_BREAKER', true),
|
||||
attributeMappers: [], // Will be set to defaults in DiscoveryServiceBootstrapper
|
||||
targetInterfaces: [], // Will be set to defaults in DiscoveryServiceBootstrapper
|
||||
|
||||
// Memory-Management-Konfiguration
|
||||
enableAdaptiveChunking: $env->getBool('DISCOVERY_ENABLE_ADAPTIVE_CHUNKING', true),
|
||||
chunkSize: $env->getInt('DISCOVERY_CHUNK_SIZE', self::DEFAULT_CHUNK_SIZE),
|
||||
memoryThresholdHigh: $env->getFloat('DISCOVERY_MEMORY_THRESHOLD_HIGH', self::DEFAULT_MEMORY_THRESHOLD_HIGH),
|
||||
memoryThresholdCritical: $env->getFloat('DISCOVERY_MEMORY_THRESHOLD_CRITICAL', self::DEFAULT_MEMORY_THRESHOLD_CRITICAL),
|
||||
|
||||
// Asynchrone Verarbeitung
|
||||
enableAsyncProcessing: $env->getBool('DISCOVERY_ENABLE_ASYNC', false),
|
||||
workerCount: $env->has('DISCOVERY_WORKER_COUNT') ? $env->getInt('DISCOVERY_WORKER_COUNT') : null,
|
||||
enableWorkloadBalancing: $env->getBool('DISCOVERY_ENABLE_WORKLOAD_BALANCING', true),
|
||||
|
||||
// Fehlerbehandlung
|
||||
maxRetries: $env->getInt('DISCOVERY_MAX_RETRIES', self::DEFAULT_MAX_RETRIES),
|
||||
logLevel: $env->getString('DISCOVERY_LOG_LEVEL', 'info'),
|
||||
continueOnError: $env->getBool('DISCOVERY_CONTINUE_ON_ERROR', true),
|
||||
|
||||
// Cache-Validierung
|
||||
cacheTtl: $env->getInt('DISCOVERY_CACHE_TTL', self::DEFAULT_CACHE_TTL),
|
||||
enableChecksumValidation: $env->getBool('DISCOVERY_ENABLE_CHECKSUM_VALIDATION', true),
|
||||
enableCacheWarming: $env->getBool('DISCOVERY_ENABLE_CACHE_WARMING', false)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt eine Entwicklungskonfiguration mit detailliertem Logging und Robustheit
|
||||
*/
|
||||
public static function development(): self
|
||||
{
|
||||
return new self(
|
||||
// Basis-Konfiguration
|
||||
useCache: true,
|
||||
showProgress: true,
|
||||
useContextAwareInitializers: true,
|
||||
enableCircuitBreaker: true,
|
||||
|
||||
// Memory-Management-Konfiguration
|
||||
enableAdaptiveChunking: true,
|
||||
chunkSize: 50, // Kleinere Chunks für bessere Responsiveness
|
||||
|
||||
// Asynchrone Verarbeitung
|
||||
enableAsyncProcessing: false, // Synchron für einfacheres Debugging
|
||||
|
||||
// Fehlerbehandlung
|
||||
maxRetries: 3,
|
||||
logLevel: 'debug', // Detailliertes Logging
|
||||
continueOnError: true,
|
||||
|
||||
// Cache-Validierung
|
||||
cacheTtl: 1800, // 30 Minuten (kürzere Zeit für Entwicklung)
|
||||
enableChecksumValidation: true,
|
||||
enableCacheWarming: false
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt eine Produktionskonfiguration mit optimaler Performance
|
||||
*/
|
||||
public static function production(): self
|
||||
{
|
||||
return new self(
|
||||
// Basis-Konfiguration
|
||||
useCache: true,
|
||||
showProgress: false,
|
||||
useContextAwareInitializers: true,
|
||||
enableCircuitBreaker: true,
|
||||
|
||||
// Memory-Management-Konfiguration
|
||||
enableAdaptiveChunking: true,
|
||||
chunkSize: 100,
|
||||
|
||||
// Asynchrone Verarbeitung
|
||||
enableAsyncProcessing: true, // Asynchron für bessere Performance
|
||||
workerCount: null, // Automatisch bestimmen
|
||||
enableWorkloadBalancing: true,
|
||||
|
||||
// Fehlerbehandlung
|
||||
maxRetries: 2,
|
||||
logLevel: 'info',
|
||||
continueOnError: true,
|
||||
|
||||
// Cache-Validierung
|
||||
cacheTtl: 86400, // 24 Stunden
|
||||
enableChecksumValidation: true,
|
||||
enableCacheWarming: true
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt eine Konfiguration für Umgebungen mit wenig Speicher
|
||||
*/
|
||||
public static function lowMemory(): self
|
||||
{
|
||||
return new self(
|
||||
// Basis-Konfiguration
|
||||
useCache: true,
|
||||
showProgress: false,
|
||||
useContextAwareInitializers: false, // Reduziert Speicherverbrauch
|
||||
enableCircuitBreaker: true,
|
||||
|
||||
// Memory-Management-Konfiguration
|
||||
enableAdaptiveChunking: true,
|
||||
chunkSize: 25, // Sehr kleine Chunks
|
||||
memoryThresholdHigh: 0.7, // Früher eingreifen
|
||||
memoryThresholdCritical: 0.8,
|
||||
|
||||
// Asynchrone Verarbeitung
|
||||
enableAsyncProcessing: false, // Synchron für weniger Speicherverbrauch
|
||||
|
||||
// Fehlerbehandlung
|
||||
maxRetries: 1, // Minimale Retries
|
||||
logLevel: 'warning', // Minimales Logging
|
||||
continueOnError: true,
|
||||
|
||||
// Cache-Validierung
|
||||
cacheTtl: 172800, // 48 Stunden (längere Cache-Zeit)
|
||||
enableChecksumValidation: false, // Deaktiviert für weniger Speicherverbrauch
|
||||
enableCacheWarming: false
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt eine Konfiguration für Hochleistungsserver
|
||||
*/
|
||||
public static function highPerformance(): self
|
||||
{
|
||||
return new self(
|
||||
// Basis-Konfiguration
|
||||
useCache: true,
|
||||
showProgress: false,
|
||||
useContextAwareInitializers: true,
|
||||
enableCircuitBreaker: true,
|
||||
|
||||
// Memory-Management-Konfiguration
|
||||
enableAdaptiveChunking: true,
|
||||
chunkSize: 200, // Größere Chunks
|
||||
|
||||
// Asynchrone Verarbeitung
|
||||
enableAsyncProcessing: true,
|
||||
workerCount: 8, // Feste Anzahl für optimale Parallelisierung
|
||||
enableWorkloadBalancing: true,
|
||||
|
||||
// Fehlerbehandlung
|
||||
maxRetries: 2,
|
||||
logLevel: 'info',
|
||||
continueOnError: true,
|
||||
|
||||
// Cache-Validierung
|
||||
cacheTtl: 43200, // 12 Stunden
|
||||
enableChecksumValidation: true,
|
||||
enableCacheWarming: true
|
||||
);
|
||||
}
|
||||
}
|
||||
285
src/Framework/Config/EncryptedEnvLoader.php
Normal file
285
src/Framework/Config/EncryptedEnvLoader.php
Normal file
@@ -0,0 +1,285 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Config;
|
||||
|
||||
use App\Framework\Encryption\EncryptionFactory;
|
||||
use App\Framework\Filesystem\FilePath;
|
||||
use App\Framework\Random\RandomGenerator;
|
||||
|
||||
/**
|
||||
* Enhanced environment loader with encryption support
|
||||
*/
|
||||
final readonly class EncryptedEnvLoader
|
||||
{
|
||||
public function __construct(
|
||||
private EncryptionFactory $encryptionFactory,
|
||||
private RandomGenerator $randomGenerator
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Load environment from multiple files with encryption support
|
||||
*/
|
||||
public function loadEnvironment(FilePath|string $basePath, ?string $encryptionKey = null): Environment
|
||||
{
|
||||
$variables = [];
|
||||
$baseDir = $basePath instanceof FilePath ? $basePath : FilePath::create($basePath);
|
||||
|
||||
// Load base .env file
|
||||
$envFile = $baseDir->join('.env');
|
||||
if ($envFile->exists()) {
|
||||
$variables = array_merge($variables, $this->parseEnvFile($envFile));
|
||||
}
|
||||
|
||||
// Load .env.secrets file if it exists and encryption key is provided
|
||||
$secretsFile = $baseDir->join('.env.secrets');
|
||||
if ($secretsFile->exists() && $encryptionKey !== null) {
|
||||
$secretsVariables = $this->parseEnvFile($secretsFile);
|
||||
$variables = array_merge($variables, $secretsVariables);
|
||||
}
|
||||
|
||||
// Load environment-specific files
|
||||
$appEnv = $variables['APP_ENV'] ?? $_ENV['APP_ENV'] ?? 'development';
|
||||
$envSpecificFile = $baseDir->join(".env.{$appEnv}");
|
||||
if ($envSpecificFile->exists()) {
|
||||
$envSpecificVariables = $this->parseEnvFile($envSpecificFile);
|
||||
$variables = array_merge($variables, $envSpecificVariables);
|
||||
}
|
||||
|
||||
return new Environment($variables);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a single .env file
|
||||
*/
|
||||
private function parseEnvFile(FilePath|string $filePath): array
|
||||
{
|
||||
$variables = [];
|
||||
$file = $filePath instanceof FilePath ? $filePath : FilePath::create($filePath);
|
||||
|
||||
if (! $file->isReadable()) {
|
||||
return $variables;
|
||||
}
|
||||
|
||||
$lines = file($file->toString(), FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
if ($lines === false) {
|
||||
return $variables;
|
||||
}
|
||||
|
||||
foreach ($lines as $lineNumber => $line) {
|
||||
$line = trim($line);
|
||||
|
||||
// Skip comments and empty lines
|
||||
if (str_starts_with($line, '#') || ! str_contains($line, '=')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse key=value pairs
|
||||
$equalPos = strpos($line, '=');
|
||||
if ($equalPos === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$name = trim(substr($line, 0, $equalPos));
|
||||
$value = trim(substr($line, $equalPos + 1));
|
||||
|
||||
// Remove surrounding quotes
|
||||
$value = $this->removeQuotes($value);
|
||||
|
||||
// Handle multiline values (basic support)
|
||||
if (str_ends_with($value, '\\')) {
|
||||
$value = rtrim($value, '\\');
|
||||
// In a full implementation, you'd continue reading the next line
|
||||
}
|
||||
|
||||
$variables[$name] = $this->castValue($value);
|
||||
}
|
||||
|
||||
return $variables;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove surrounding quotes from values
|
||||
*/
|
||||
private function removeQuotes(string $value): string
|
||||
{
|
||||
// Remove double quotes
|
||||
if (str_starts_with($value, '"') && str_ends_with($value, '"') && strlen($value) > 1) {
|
||||
return substr($value, 1, -1);
|
||||
}
|
||||
|
||||
// Remove single quotes
|
||||
if (str_starts_with($value, "'") && str_ends_with($value, "'") && strlen($value) > 1) {
|
||||
return substr($value, 1, -1);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cast string values to appropriate types
|
||||
*/
|
||||
private function castValue(string $value): mixed
|
||||
{
|
||||
return match (strtolower($value)) {
|
||||
'true' => true,
|
||||
'false' => false,
|
||||
'null' => null,
|
||||
default => is_numeric($value) ? (str_contains($value, '.') ? (float) $value : (int) $value) : $value
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a .env.secrets template file
|
||||
*/
|
||||
public function generateSecretsTemplate(FilePath|string $basePath, array $secretKeys = []): FilePath
|
||||
{
|
||||
$template = "# .env.secrets - Encrypted secrets file\n";
|
||||
$template .= "# Generated on " . date('Y-m-d H:i:s') . "\n";
|
||||
$template .= "# \n";
|
||||
$template .= "# Instructions:\n";
|
||||
$template .= "# 1. Set ENCRYPTION_KEY in your main .env file\n";
|
||||
$template .= "# 2. Use SecretManager::encryptSecret() to encrypt sensitive values\n";
|
||||
$template .= "# 3. Store encrypted values in this file with ENC[...] format\n";
|
||||
$template .= "# \n";
|
||||
$template .= "# Example:\n";
|
||||
$template .= "# SECRET_API_KEY=ENC[base64encodedencryptedvalue]\n";
|
||||
$template .= "# SECRET_DATABASE_PASSWORD=ENC[anotherencryptedvalue]\n";
|
||||
$template .= "\n";
|
||||
|
||||
// Add predefined secret keys
|
||||
$defaultSecretKeys = [
|
||||
'SECRET_ENCRYPTION_KEY',
|
||||
'SECRET_DATABASE_PASSWORD',
|
||||
'SECRET_API_KEY',
|
||||
'SECRET_JWT_SECRET',
|
||||
'SECRET_OAUTH_CLIENT_SECRET',
|
||||
'SECRET_SMTP_PASSWORD',
|
||||
'SECRET_REDIS_PASSWORD',
|
||||
];
|
||||
|
||||
$allSecretKeys = array_unique(array_merge($defaultSecretKeys, $secretKeys));
|
||||
|
||||
foreach ($allSecretKeys as $key) {
|
||||
$template .= "# {$key}=ENC[your_encrypted_value_here]\n";
|
||||
}
|
||||
|
||||
$baseDir = $basePath instanceof FilePath ? $basePath : FilePath::create($basePath);
|
||||
$filePath = $baseDir->join('.env.secrets');
|
||||
file_put_contents($filePath->toString(), $template);
|
||||
|
||||
return $filePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt secrets in an existing .env file
|
||||
*/
|
||||
public function encryptSecretsInFile(FilePath|string $filePath, string $encryptionKey, array $keysToEncrypt = []): int
|
||||
{
|
||||
$file = $filePath instanceof FilePath ? $filePath : FilePath::create($filePath);
|
||||
|
||||
if (! $file->exists() || ! $file->isReadable()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$encryption = $this->encryptionFactory->createBest($encryptionKey);
|
||||
$lines = file($file->toString(), FILE_IGNORE_NEW_LINES);
|
||||
if ($lines === false) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$encryptedCount = 0;
|
||||
$newLines = [];
|
||||
|
||||
foreach ($lines as $line) {
|
||||
$trimmedLine = trim($line);
|
||||
|
||||
// Skip comments and empty lines
|
||||
if (str_starts_with($trimmedLine, '#') || ! str_contains($trimmedLine, '=')) {
|
||||
$newLines[] = $line;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$equalPos = strpos($trimmedLine, '=');
|
||||
if ($equalPos === false) {
|
||||
$newLines[] = $line;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$name = trim(substr($trimmedLine, 0, $equalPos));
|
||||
$value = trim(substr($trimmedLine, $equalPos + 1));
|
||||
|
||||
// Check if this key should be encrypted
|
||||
$shouldEncrypt = empty($keysToEncrypt) ? str_starts_with($name, 'SECRET_') : in_array($name, $keysToEncrypt, true);
|
||||
|
||||
if ($shouldEncrypt && ! $encryption->isEncrypted($value)) {
|
||||
$value = $this->removeQuotes($value);
|
||||
$encryptedValue = $encryption->encrypt($value);
|
||||
$newLines[] = "{$name}={$encryptedValue}";
|
||||
$encryptedCount++;
|
||||
} else {
|
||||
$newLines[] = $line;
|
||||
}
|
||||
}
|
||||
|
||||
// Write back to file
|
||||
file_put_contents($file->toString(), implode("\n", $newLines));
|
||||
|
||||
return $encryptedCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate encryption setup
|
||||
*/
|
||||
public function validateEncryptionSetup(FilePath|string $basePath, ?string $encryptionKey = null): array
|
||||
{
|
||||
$issues = [];
|
||||
|
||||
// Check if encryption key is provided
|
||||
if ($encryptionKey === null) {
|
||||
$issues[] = [
|
||||
'type' => 'missing_key',
|
||||
'message' => 'No encryption key provided',
|
||||
'severity' => 'high',
|
||||
];
|
||||
|
||||
return $issues;
|
||||
}
|
||||
|
||||
// Check key strength
|
||||
if (strlen($encryptionKey) < 32) {
|
||||
$issues[] = [
|
||||
'type' => 'weak_key',
|
||||
'message' => 'Encryption key should be at least 32 characters',
|
||||
'severity' => 'high',
|
||||
];
|
||||
}
|
||||
|
||||
// Check if .env.secrets exists
|
||||
$baseDir = $basePath instanceof FilePath ? $basePath : FilePath::create($basePath);
|
||||
$secretsFile = $baseDir->join('.env.secrets');
|
||||
if (! $secretsFile->exists()) {
|
||||
$issues[] = [
|
||||
'type' => 'missing_secrets_file',
|
||||
'message' => '.env.secrets file not found',
|
||||
'severity' => 'medium',
|
||||
];
|
||||
}
|
||||
|
||||
// Check encryption availability
|
||||
$availableMethods = $this->encryptionFactory->getAvailableMethods();
|
||||
if (! in_array('AES', $availableMethods, true)) {
|
||||
$issues[] = [
|
||||
'type' => 'weak_encryption',
|
||||
'message' => 'OpenSSL not available, using basic encryption',
|
||||
'severity' => 'medium',
|
||||
];
|
||||
}
|
||||
|
||||
return $issues;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,40 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Config;
|
||||
|
||||
enum EnvKey: string
|
||||
{
|
||||
case APP_NAME = 'APP_NAME';
|
||||
case APP_DEBUG = 'APP_DEBUG';
|
||||
case APP_ENV = 'APP_ENV';
|
||||
case APP_KEY = 'APP_KEY';
|
||||
case APP_TIMEZONE = 'APP_TIMEZONE';
|
||||
case APP_LOCALE = 'APP_LOCALE';
|
||||
|
||||
// Feature Flags
|
||||
case ENABLE_CONTEXT_AWARE_INITIALIZERS = 'ENABLE_CONTEXT_AWARE_INITIALIZERS';
|
||||
|
||||
// Database
|
||||
case DB_DRIVER = 'DB_DRIVER';
|
||||
case DB_HOST = 'DB_HOST';
|
||||
case DB_PORT = 'DB_PORT';
|
||||
case DB_DATABASE = 'DB_DATABASE';
|
||||
case DB_USERNAME = 'DB_USERNAME';
|
||||
case DB_PASSWORD = 'DB_PASSWORD';
|
||||
case DB_CHARSET = 'DB_CHARSET';
|
||||
|
||||
// External APIs
|
||||
case SHOPIFY_WEBHOOK_SECRET = 'SHOPIFY_WEBHOOK_SECRET';
|
||||
case RAPIDMAIL_USERNAME = 'RAPIDMAIL_USERNAME';
|
||||
case RAPIDMAIL_PASSWORD = 'RAPIDMAIL_PASSWORD';
|
||||
case RAPIDMAIL_TEST_MODE = 'RAPIDMAIL_TEST_MODE';
|
||||
|
||||
// ETag Configuration
|
||||
case ETAG_ENABLED = 'ETAG_ENABLED';
|
||||
case ETAG_PREFER_WEAK = 'ETAG_PREFER_WEAK';
|
||||
case ETAG_MIDDLEWARE_ENABLED = 'ETAG_MIDDLEWARE_ENABLED';
|
||||
case ETAG_EXCLUDE_PATHS = 'ETAG_EXCLUDE_PATHS';
|
||||
case ETAG_EXCLUDE_CONTENT_TYPES = 'ETAG_EXCLUDE_CONTENT_TYPES';
|
||||
}
|
||||
|
||||
@@ -5,16 +5,19 @@ declare(strict_types=1);
|
||||
namespace App\Framework\Config;
|
||||
|
||||
use App\Framework\Config\Exceptions\RequiredEnvironmentVariableException;
|
||||
use App\Framework\Filesystem\FilePath;
|
||||
|
||||
final readonly class Environment
|
||||
{
|
||||
public function __construct(
|
||||
private array $variables = []
|
||||
){}
|
||||
) {
|
||||
}
|
||||
|
||||
public function get(EnvKey|string $key, mixed $default = null): mixed
|
||||
{
|
||||
$key = $this->keyToString($key);
|
||||
|
||||
// Priorität: 1. System ENV, 2. Loaded variables, 3. Default
|
||||
return $_ENV[$key] ?? $_SERVER[$key] ?? getenv($key) ?: $this->variables[$key] ?? $default;
|
||||
}
|
||||
@@ -26,15 +29,24 @@ final readonly class Environment
|
||||
if ($value === null) {
|
||||
throw new RequiredEnvironmentVariableException($key);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
public function getInt(EnvKey|string $key, int $default = 0): int
|
||||
{
|
||||
$key = $this->keyToString($key);
|
||||
|
||||
return (int) $this->get($key, $default);
|
||||
}
|
||||
|
||||
public function getFloat(EnvKey|string $key, float $default = 0.0): float
|
||||
{
|
||||
$key = $this->keyToString($key);
|
||||
|
||||
return (float) $this->get($key, $default);
|
||||
}
|
||||
|
||||
public function getBool(EnvKey|string $key, bool $default = false): bool
|
||||
{
|
||||
$key = $this->keyToString($key);
|
||||
@@ -46,18 +58,21 @@ final readonly class Environment
|
||||
default => $default
|
||||
};
|
||||
}
|
||||
|
||||
return (bool) $value;
|
||||
}
|
||||
|
||||
public function getString(EnvKey|string $key, string $default = ''): string
|
||||
{
|
||||
$key = $this->keyToString($key);
|
||||
|
||||
return (string) $this->get($key, $default);
|
||||
}
|
||||
|
||||
public function getEnum(EnvKey|string $key, string $enumClass, \BackedEnum $default):object
|
||||
public function getEnum(EnvKey|string $key, string $enumClass, \BackedEnum $default): object
|
||||
{
|
||||
$key = $this->keyToString($key);
|
||||
|
||||
return forward_static_call([$enumClass, 'tryFrom'], $this->get($key, $default));
|
||||
#$enumClass::tryFrom($this->get($key, $default));
|
||||
}
|
||||
@@ -65,6 +80,7 @@ final readonly class Environment
|
||||
public function has(EnvKey|string $key): bool
|
||||
{
|
||||
$key = $this->keyToString($key);
|
||||
|
||||
return $this->get($key) !== null;
|
||||
}
|
||||
|
||||
@@ -80,15 +96,16 @@ final readonly class Environment
|
||||
/**
|
||||
* Factory method für .env file loading
|
||||
*/
|
||||
public static function fromFile(string $envPath): self
|
||||
public static function fromFile(FilePath|string $envPath): self
|
||||
{
|
||||
$variables = [];
|
||||
$filePath = $envPath instanceof FilePath ? $envPath : FilePath::create($envPath);
|
||||
|
||||
if (file_exists($envPath) && is_readable($envPath)) {
|
||||
$lines = file($envPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
if ($filePath->exists() && $filePath->isReadable()) {
|
||||
$lines = file($filePath->toString(), FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
|
||||
foreach ($lines as $line) {
|
||||
if (str_starts_with(trim($line), '#') || !str_contains($line, '=')) {
|
||||
if (str_starts_with(trim($line), '#') || ! str_contains($line, '=')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -118,11 +135,12 @@ final readonly class Environment
|
||||
};
|
||||
}
|
||||
|
||||
public function keyToString(EnvKey|string $key):string
|
||||
public function keyToString(EnvKey|string $key): string
|
||||
{
|
||||
if(is_string($key)) {
|
||||
if (is_string($key)) {
|
||||
return $key;
|
||||
}
|
||||
|
||||
return $key->value;
|
||||
}
|
||||
|
||||
@@ -133,6 +151,7 @@ final readonly class Environment
|
||||
{
|
||||
$variables = $this->variables;
|
||||
$variables[$key] = $value;
|
||||
|
||||
return new self($variables);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,49 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Config;
|
||||
|
||||
enum EnvironmentType: string
|
||||
{
|
||||
case DEV = 'development';
|
||||
case STAGING = 'staging';
|
||||
case PROD = 'production';
|
||||
|
||||
public function isProduction(): bool
|
||||
{
|
||||
return $this === self::PROD;
|
||||
}
|
||||
|
||||
public function isDevelopment(): bool
|
||||
{
|
||||
return $this === self::DEV;
|
||||
}
|
||||
|
||||
public function isStaging(): bool
|
||||
{
|
||||
return $this === self::STAGING;
|
||||
}
|
||||
|
||||
public function isDebugEnabled(): bool
|
||||
{
|
||||
return $this === self::DEV;
|
||||
}
|
||||
|
||||
public function isProductionLike(): bool
|
||||
{
|
||||
return $this === self::PROD || $this === self::STAGING;
|
||||
}
|
||||
|
||||
public static function fromEnvironment(Environment $env): self
|
||||
{
|
||||
$envValue = $env->getString('APP_ENV', 'development');
|
||||
|
||||
return match(strtolower($envValue)) {
|
||||
'production', 'prod' => self::PROD,
|
||||
'staging', 'stage' => self::STAGING,
|
||||
'development', 'dev', 'local' => self::DEV,
|
||||
default => self::DEV
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Config\Exceptions;
|
||||
|
||||
use App\Framework\Exception\ExceptionContext;
|
||||
use App\Framework\Exception\FrameworkException;
|
||||
|
||||
class RequiredEnvironmentVariableException extends FrameworkException
|
||||
{
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
*/
|
||||
public function __construct(string $key)
|
||||
{
|
||||
parent::__construct("Required environment variable '$key' is not set.");
|
||||
parent::__construct(
|
||||
message: "Required environment variable '$key' is not set.",
|
||||
context: ExceptionContext::forOperation('config_validation', 'Configuration')
|
||||
->withData(['environment_variable' => $key])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
24
src/Framework/Config/External/ExternalApiConfig.php
vendored
Normal file
24
src/Framework/Config/External/ExternalApiConfig.php
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Config\External;
|
||||
|
||||
use App\Framework\Config\Environment;
|
||||
|
||||
final readonly class ExternalApiConfig
|
||||
{
|
||||
public function __construct(
|
||||
public ShopifyConfig $shopify,
|
||||
public RapidMailConfig $rapidMail,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function fromEnvironment(Environment $env): self
|
||||
{
|
||||
return new self(
|
||||
shopify: ShopifyConfig::fromEnvironment($env),
|
||||
rapidMail: RapidMailConfig::fromEnvironment($env),
|
||||
);
|
||||
}
|
||||
}
|
||||
27
src/Framework/Config/External/RapidMailConfig.php
vendored
Normal file
27
src/Framework/Config/External/RapidMailConfig.php
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Config\External;
|
||||
|
||||
use App\Framework\Config\Environment;
|
||||
use App\Framework\Config\EnvKey;
|
||||
|
||||
final readonly class RapidMailConfig
|
||||
{
|
||||
public function __construct(
|
||||
public string $username,
|
||||
public string $password,
|
||||
public bool $testMode,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function fromEnvironment(Environment $env): self
|
||||
{
|
||||
return new self(
|
||||
username: $env->getRequired(EnvKey::RAPIDMAIL_USERNAME),
|
||||
password: $env->getRequired(EnvKey::RAPIDMAIL_PASSWORD),
|
||||
testMode: $env->getBool(EnvKey::RAPIDMAIL_TEST_MODE, true),
|
||||
);
|
||||
}
|
||||
}
|
||||
23
src/Framework/Config/External/ShopifyConfig.php
vendored
Normal file
23
src/Framework/Config/External/ShopifyConfig.php
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Config\External;
|
||||
|
||||
use App\Framework\Config\Environment;
|
||||
use App\Framework\Config\EnvKey;
|
||||
|
||||
final readonly class ShopifyConfig
|
||||
{
|
||||
public function __construct(
|
||||
public string $webhookSecret,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function fromEnvironment(Environment $env): self
|
||||
{
|
||||
return new self(
|
||||
webhookSecret: $env->getRequired(EnvKey::SHOPIFY_WEBHOOK_SECRET),
|
||||
);
|
||||
}
|
||||
}
|
||||
248
src/Framework/Config/SecretManager.php
Normal file
248
src/Framework/Config/SecretManager.php
Normal file
@@ -0,0 +1,248 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Config;
|
||||
|
||||
use App\Framework\Config\Exceptions\RequiredEnvironmentVariableException;
|
||||
use App\Framework\Config\ValueObjects\SecurityContext;
|
||||
use App\Framework\Encryption\EncryptionInterface;
|
||||
use App\Framework\Http\ServerEnvironment;
|
||||
use App\Framework\Http\ServerKey;
|
||||
use App\Framework\Random\RandomGenerator;
|
||||
use App\Framework\UserAgent\UserAgent;
|
||||
|
||||
/**
|
||||
* Secure secrets management using pluggable encryption
|
||||
*/
|
||||
final readonly class SecretManager
|
||||
{
|
||||
private const string SECRET_PREFIX = 'SECRET_';
|
||||
|
||||
public function __construct(
|
||||
private Environment $environment,
|
||||
private EncryptionInterface $encryption,
|
||||
private ServerEnvironment $serverEnvironment,
|
||||
private RandomGenerator $randomGenerator
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a secret value, automatically decrypting if necessary
|
||||
*/
|
||||
public function getSecret(EnvKey|string $key, mixed $default = null): mixed
|
||||
{
|
||||
$value = $this->environment->get($key, $default);
|
||||
|
||||
if ($value === null) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
if (is_string($value) && $this->encryption->isEncrypted($value)) {
|
||||
$this->auditSecretAccess($this->environment->keyToString($key), 'read');
|
||||
|
||||
return $this->encryption->decrypt($value);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a required secret, throwing exception if not found
|
||||
*/
|
||||
public function getRequiredSecret(EnvKey|string $key): mixed
|
||||
{
|
||||
$value = $this->getSecret($key);
|
||||
|
||||
if ($value === null) {
|
||||
$keyString = $this->environment->keyToString($key);
|
||||
|
||||
throw new RequiredEnvironmentVariableException($keyString);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt a secret value for storage
|
||||
*/
|
||||
public function encryptSecret(string $value): string
|
||||
{
|
||||
return $this->encryption->encrypt($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a value is encrypted
|
||||
*/
|
||||
public function isEncrypted(mixed $value): bool
|
||||
{
|
||||
return is_string($value) && $this->encryption->isEncrypted($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all secret keys (those starting with SECRET_PREFIX)
|
||||
*/
|
||||
public function getSecretKeys(): array
|
||||
{
|
||||
$allVars = $this->environment->all();
|
||||
$secretKeys = [];
|
||||
|
||||
foreach (array_keys($allVars) as $key) {
|
||||
if (str_starts_with($key, self::SECRET_PREFIX)) {
|
||||
$secretKeys[] = $key;
|
||||
}
|
||||
}
|
||||
|
||||
return $secretKeys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotate encryption for all secrets with new encryption instance
|
||||
*/
|
||||
public function rotateSecrets(EncryptionInterface $newEncryption): array
|
||||
{
|
||||
$secretKeys = $this->getSecretKeys();
|
||||
$rotatedSecrets = [];
|
||||
|
||||
foreach ($secretKeys as $key) {
|
||||
$currentValue = $this->environment->get($key);
|
||||
|
||||
if ($this->isEncrypted($currentValue)) {
|
||||
// Decrypt with old encryption, encrypt with new encryption
|
||||
$decrypted = $this->encryption->decrypt($currentValue);
|
||||
$rotatedSecrets[$key] = $newEncryption->encrypt($decrypted);
|
||||
|
||||
$this->auditSecretAccess($key, 'rotate');
|
||||
}
|
||||
}
|
||||
|
||||
return $rotatedSecrets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a secure token for API keys, passwords, etc.
|
||||
*/
|
||||
public function generateSecureToken(int $length = 32): string
|
||||
{
|
||||
$bytes = $this->randomGenerator->bytes($length);
|
||||
|
||||
return bin2hex($bytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a secure password with mixed character types
|
||||
*/
|
||||
public function generateSecurePassword(int $length = 16): string
|
||||
{
|
||||
$characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*';
|
||||
$charactersLength = strlen($characters);
|
||||
$password = '';
|
||||
|
||||
for ($i = 0; $i < $length; $i++) {
|
||||
$randomIndex = $this->randomGenerator->int(0, $charactersLength - 1);
|
||||
$password .= $characters[$randomIndex];
|
||||
}
|
||||
|
||||
return $password;
|
||||
}
|
||||
|
||||
/**
|
||||
* Audit log for secret access (for compliance)
|
||||
*/
|
||||
public function auditSecretAccess(string $key, string $action = 'read'): void
|
||||
{
|
||||
$timestamp = date('Y-m-d H:i:s');
|
||||
$user = $this->serverEnvironment->get(ServerKey::REMOTE_USER, 'unknown');
|
||||
$clientIp = (string)$this->serverEnvironment->getClientIp();
|
||||
$userAgent = $this->maskUserAgent($this->serverEnvironment->getUserAgent());
|
||||
|
||||
$logEntry = sprintf(
|
||||
'[SECRETS] %s - Action: %s, Key: %s, User: %s, IP: %s, Method: %s, UA: %s',
|
||||
$timestamp,
|
||||
$action,
|
||||
$key,
|
||||
$user,
|
||||
$clientIp,
|
||||
$this->encryption->getMethod(),
|
||||
$userAgent
|
||||
);
|
||||
|
||||
// Log to system logs (in production, this should go to a secure audit system)
|
||||
error_log($logEntry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mask sensitive values for logging
|
||||
*/
|
||||
public function maskSecret(string $secret): string
|
||||
{
|
||||
$length = strlen($secret);
|
||||
|
||||
if ($length <= 4) {
|
||||
return str_repeat('*', $length);
|
||||
}
|
||||
|
||||
return substr($secret, 0, 2) . str_repeat('*', $length - 4) . substr($secret, -2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mask user agent for privacy in logs
|
||||
*/
|
||||
private function maskUserAgent(UserAgent $userAgent): string
|
||||
{
|
||||
$userAgentString = $userAgent->value;
|
||||
|
||||
if (strlen($userAgentString) <= 20) {
|
||||
return $userAgentString;
|
||||
}
|
||||
|
||||
return substr($userAgentString, 0, 20) . '...';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current context is secure (HTTPS)
|
||||
*/
|
||||
public function isSecureContext(): bool
|
||||
{
|
||||
return $this->serverEnvironment->isHttps();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get security context information
|
||||
*/
|
||||
public function getSecurityContext(): SecurityContext
|
||||
{
|
||||
return SecurityContext::create(
|
||||
isHttps: $this->serverEnvironment->isHttps(),
|
||||
clientIp: $this->serverEnvironment->getClientIp(),
|
||||
maskedUserAgent: $this->maskUserAgent($this->serverEnvironment->getUserAgent()),
|
||||
serverName: $this->serverEnvironment->getServerName(),
|
||||
encryptionMethod: $this->encryption->getMethod()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that secrets are properly encrypted in non-secure contexts
|
||||
*/
|
||||
public function validateSecretsForContext(): array
|
||||
{
|
||||
$issues = [];
|
||||
$secretKeys = $this->getSecretKeys();
|
||||
$isSecure = $this->isSecureContext();
|
||||
|
||||
foreach ($secretKeys as $key) {
|
||||
$value = $this->environment->get($key);
|
||||
|
||||
if (! $this->isEncrypted($value)) {
|
||||
$issues[] = [
|
||||
'key' => $key,
|
||||
'issue' => 'not_encrypted',
|
||||
'severity' => $isSecure ? 'medium' : 'high',
|
||||
'recommendation' => 'Encrypt this secret using SecretManager::encryptSecret()',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $issues;
|
||||
}
|
||||
}
|
||||
92
src/Framework/Config/SecurityConfig.php
Normal file
92
src/Framework/Config/SecurityConfig.php
Normal file
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Config;
|
||||
|
||||
/**
|
||||
* Security configuration that adapts to environment
|
||||
*/
|
||||
final readonly class SecurityConfig
|
||||
{
|
||||
public function __construct(
|
||||
public string $appKey = '',
|
||||
public bool $enableSecurityHeaders = true,
|
||||
public bool $enableCsrfProtection = true,
|
||||
public bool $enableRateLimiting = true,
|
||||
public int $rateLimitPerMinute = 60,
|
||||
public int $rateLimitBurstSize = 10,
|
||||
public bool $logSecurityEvents = true,
|
||||
public bool $enableStrictMode = false,
|
||||
public array $allowedHosts = [],
|
||||
public int $sessionLifetime = 3600,
|
||||
public bool $secureSessionCookies = true,
|
||||
public bool $httpOnlySessionCookies = true
|
||||
) {
|
||||
}
|
||||
|
||||
public static function forEnvironment(EnvironmentType $environment, Environment $env): self
|
||||
{
|
||||
return match($environment) {
|
||||
EnvironmentType::PROD => self::production($env),
|
||||
EnvironmentType::STAGING => self::staging($env),
|
||||
EnvironmentType::DEV => self::development($env),
|
||||
};
|
||||
}
|
||||
|
||||
private static function production(Environment $env): self
|
||||
{
|
||||
return new self(
|
||||
appKey: $env->getRequired(EnvKey::APP_KEY),
|
||||
enableSecurityHeaders: true,
|
||||
enableCsrfProtection: true,
|
||||
enableRateLimiting: true,
|
||||
rateLimitPerMinute: $env->getInt('SECURITY_RATE_LIMIT_PER_MINUTE', 30),
|
||||
rateLimitBurstSize: $env->getInt('SECURITY_RATE_LIMIT_BURST', 5),
|
||||
logSecurityEvents: true,
|
||||
enableStrictMode: true,
|
||||
allowedHosts: $env->getString('SECURITY_ALLOWED_HOSTS', '') ?
|
||||
explode(',', $env->getString('SECURITY_ALLOWED_HOSTS')) : [],
|
||||
sessionLifetime: $env->getInt('SESSION_LIFETIME', 1800), // 30 minutes
|
||||
secureSessionCookies: true,
|
||||
httpOnlySessionCookies: true
|
||||
);
|
||||
}
|
||||
|
||||
private static function staging(Environment $env): self
|
||||
{
|
||||
return new self(
|
||||
appKey: $env->getRequired(EnvKey::APP_KEY),
|
||||
enableSecurityHeaders: true,
|
||||
enableCsrfProtection: true,
|
||||
enableRateLimiting: true,
|
||||
rateLimitPerMinute: $env->getInt('SECURITY_RATE_LIMIT_PER_MINUTE', 60),
|
||||
rateLimitBurstSize: $env->getInt('SECURITY_RATE_LIMIT_BURST', 10),
|
||||
logSecurityEvents: true,
|
||||
enableStrictMode: true,
|
||||
allowedHosts: $env->getString('SECURITY_ALLOWED_HOSTS', '') ?
|
||||
explode(',', $env->getString('SECURITY_ALLOWED_HOSTS')) : [],
|
||||
sessionLifetime: $env->getInt('SESSION_LIFETIME', 3600), // 1 hour
|
||||
secureSessionCookies: true,
|
||||
httpOnlySessionCookies: true
|
||||
);
|
||||
}
|
||||
|
||||
private static function development(Environment $env): self
|
||||
{
|
||||
return new self(
|
||||
appKey: $env->getString(EnvKey::APP_KEY, 'dev-insecure-key-32-chars-long!!!'),
|
||||
enableSecurityHeaders: true, // Security Headers IMMER aktiv
|
||||
enableCsrfProtection: true, // CSRF IMMER aktiv für Sicherheit
|
||||
enableRateLimiting: $env->getBool('SECURITY_ENABLE_RATE_LIMITING', false),
|
||||
rateLimitPerMinute: $env->getInt('SECURITY_RATE_LIMIT_PER_MINUTE', 1000),
|
||||
rateLimitBurstSize: $env->getInt('SECURITY_RATE_LIMIT_BURST', 100),
|
||||
logSecurityEvents: $env->getBool('SECURITY_LOG_EVENTS', true),
|
||||
enableStrictMode: false,
|
||||
allowedHosts: [], // Allow all in development
|
||||
sessionLifetime: $env->getInt('SESSION_LIFETIME', 86400), // 24 hours
|
||||
secureSessionCookies: false, // Allow HTTP in development
|
||||
httpOnlySessionCookies: true
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,47 +1,89 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Config;
|
||||
|
||||
use App\Framework\Context\ContextType;
|
||||
use App\Framework\Config\External\ExternalApiConfig;
|
||||
use App\Framework\Database\Config\DatabaseConfig;
|
||||
use App\Framework\Database\Config\DatabaseConfigInitializer;
|
||||
use App\Framework\DateTime\Timezone;
|
||||
use App\Framework\DI\Container;
|
||||
use App\Framework\RateLimit\RateLimitConfig;
|
||||
|
||||
final readonly class TypedConfigInitializer
|
||||
{
|
||||
public function __construct(
|
||||
private Environment $env,
|
||||
){
|
||||
#debug($env->getEnum('APP_ENV', EnvironmentType::class, EnvironmentType::PROD));
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(Container $container): TypedConfiguration
|
||||
{
|
||||
$databaseConfig = $this->createDatabaseConfig();
|
||||
$appConfig = $this->createAppConfig();
|
||||
$securityConfig = $this->createSecurityConfig($appConfig->type);
|
||||
$rateLimitConfig = $this->createRateLimitConfig($securityConfig);
|
||||
$externalApiConfig = ExternalApiConfig::fromEnvironment($this->env);
|
||||
$discoveryConfig = DiscoveryConfig::fromEnvironment($this->env);
|
||||
|
||||
$container->instance(DatabaseConfig::class, $databaseConfig);
|
||||
$container->instance(AppConfig::class, $appConfig);
|
||||
$container->instance(SecurityConfig::class, $securityConfig);
|
||||
$container->instance(RateLimitConfig::class, $rateLimitConfig);
|
||||
$container->instance(ExternalApiConfig::class, $externalApiConfig);
|
||||
$container->instance(DiscoveryConfig::class, $discoveryConfig);
|
||||
|
||||
// Validate environment configuration (non-fatal reporting)
|
||||
$validator = new ConfigValidator($this->env);
|
||||
$validator->validateAndReport();
|
||||
|
||||
return new TypedConfiguration(
|
||||
database: $databaseConfig,
|
||||
app: $appConfig,
|
||||
security: $securityConfig,
|
||||
rateLimit: $rateLimitConfig,
|
||||
externalApis: $externalApiConfig,
|
||||
discovery: $discoveryConfig,
|
||||
);
|
||||
}
|
||||
|
||||
private function createAppConfig(): AppConfig
|
||||
{
|
||||
$environmentType = EnvironmentType::fromEnvironment($this->env);
|
||||
|
||||
return new AppConfig(
|
||||
name: $this->env->getString(EnvKey::APP_NAME, 'Framework App'),
|
||||
version: $this->env->getString('APP_VERSION', '1.0.0'),
|
||||
environment: $this->env->getString(
|
||||
key: EnvKey::APP_ENV,
|
||||
default: 'production'
|
||||
),
|
||||
|
||||
debug: $this->env->getBool(EnvKey::APP_DEBUG),
|
||||
|
||||
debug: $this->env->getBool(EnvKey::APP_DEBUG, $environmentType->isDebugEnabled()),
|
||||
timezone: $this->env->getEnum(
|
||||
key: EnvKey::APP_TIMEZONE,
|
||||
enumClass: Timezone::class,
|
||||
default: Timezone::EuropeBerlin
|
||||
),
|
||||
|
||||
locale: $this->env->getString(EnvKey::APP_LOCALE, 'de'),
|
||||
type: $environmentType,
|
||||
);
|
||||
}
|
||||
|
||||
private function createSecurityConfig(EnvironmentType $environmentType): SecurityConfig
|
||||
{
|
||||
return SecurityConfig::forEnvironment($environmentType, $this->env);
|
||||
}
|
||||
|
||||
private function createDatabaseConfig(): DatabaseConfig
|
||||
{
|
||||
$initializer = new DatabaseConfigInitializer($this->env);
|
||||
|
||||
return $initializer();
|
||||
}
|
||||
|
||||
private function createRateLimitConfig(SecurityConfig $securityConfig): RateLimitConfig
|
||||
{
|
||||
return RateLimitConfig::fromSecurityConfig($securityConfig);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Config;
|
||||
|
||||
use App\Framework\Config\External\ExternalApiConfig;
|
||||
use App\Framework\Database\Config\DatabaseConfig;
|
||||
use App\Framework\RateLimit\RateLimitConfig;
|
||||
|
||||
final readonly class TypedConfiguration
|
||||
{
|
||||
public function __construct(
|
||||
#public DatabaseConfig $database,
|
||||
public DatabaseConfig $database,
|
||||
public AppConfig $app,
|
||||
)
|
||||
{
|
||||
public SecurityConfig $security,
|
||||
public RateLimitConfig $rateLimit,
|
||||
public ExternalApiConfig $externalApis,
|
||||
public DiscoveryConfig $discovery,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
119
src/Framework/Config/ValueObjects/SecurityContext.php
Normal file
119
src/Framework/Config/ValueObjects/SecurityContext.php
Normal file
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Config\ValueObjects;
|
||||
|
||||
use App\Framework\Core\ValueObjects\Timestamp;
|
||||
use App\Framework\Http\IpAddress;
|
||||
|
||||
/**
|
||||
* Value Object representing security context information
|
||||
* Used for auditing, logging, and security validation
|
||||
*/
|
||||
final readonly class SecurityContext
|
||||
{
|
||||
public function __construct(
|
||||
public bool $isHttps,
|
||||
public IpAddress $clientIp,
|
||||
public string $maskedUserAgent,
|
||||
public string $serverName,
|
||||
public string $encryptionMethod,
|
||||
public Timestamp $timestamp
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create from server environment and encryption context
|
||||
*/
|
||||
public static function create(
|
||||
bool $isHttps,
|
||||
IpAddress $clientIp,
|
||||
string $maskedUserAgent,
|
||||
string $serverName,
|
||||
string $encryptionMethod
|
||||
): self {
|
||||
return new self(
|
||||
isHttps: $isHttps,
|
||||
clientIp: $clientIp,
|
||||
maskedUserAgent: $maskedUserAgent,
|
||||
serverName: $serverName,
|
||||
encryptionMethod: $encryptionMethod,
|
||||
timestamp: Timestamp::fromFloat(microtime(true))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the context is considered secure
|
||||
*/
|
||||
public function isSecure(): bool
|
||||
{
|
||||
return $this->isHttps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get security risk level based on context
|
||||
*/
|
||||
public function getRiskLevel(): SecurityRiskLevel
|
||||
{
|
||||
if (! $this->isHttps) {
|
||||
return SecurityRiskLevel::HIGH;
|
||||
}
|
||||
|
||||
if ($this->clientIp->isPrivate()) {
|
||||
return SecurityRiskLevel::LOW;
|
||||
}
|
||||
|
||||
return SecurityRiskLevel::MEDIUM;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get context summary for logging
|
||||
*/
|
||||
public function getSummary(): string
|
||||
{
|
||||
$protocol = $this->isHttps ? 'HTTPS' : 'HTTP';
|
||||
|
||||
return sprintf(
|
||||
'%s from %s via %s (%s)',
|
||||
$protocol,
|
||||
$this->clientIp->toString(),
|
||||
$this->serverName,
|
||||
$this->encryptionMethod
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert to array for logging/serialization
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'is_https' => $this->isHttps,
|
||||
'client_ip' => $this->clientIp->toString(),
|
||||
'user_agent' => $this->maskedUserAgent,
|
||||
'server_name' => $this->serverName,
|
||||
'encryption_method' => $this->encryptionMethod,
|
||||
'timestamp' => $this->timestamp->toDateTimeString(),
|
||||
'risk_level' => $this->getRiskLevel()->value,
|
||||
'summary' => $this->getSummary(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if context is suitable for handling secrets
|
||||
*/
|
||||
public function isSecretHandlingSafe(): bool
|
||||
{
|
||||
return $this->isHttps && $this->getRiskLevel() !== SecurityRiskLevel::HIGH;
|
||||
}
|
||||
|
||||
/**
|
||||
* String representation
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->getSummary();
|
||||
}
|
||||
}
|
||||
53
src/Framework/Config/ValueObjects/SecurityRiskLevel.php
Normal file
53
src/Framework/Config/ValueObjects/SecurityRiskLevel.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Config\ValueObjects;
|
||||
|
||||
/**
|
||||
* Enum representing security risk levels
|
||||
*/
|
||||
enum SecurityRiskLevel: string
|
||||
{
|
||||
case LOW = 'low';
|
||||
case MEDIUM = 'medium';
|
||||
case HIGH = 'high';
|
||||
case CRITICAL = 'critical';
|
||||
|
||||
/**
|
||||
* Get human-readable name
|
||||
*/
|
||||
public function getDisplayName(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::LOW => 'Low Risk',
|
||||
self::MEDIUM => 'Medium Risk',
|
||||
self::HIGH => 'High Risk',
|
||||
self::CRITICAL => 'Critical Risk',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this risk level requires immediate action
|
||||
*/
|
||||
public function requiresImmediateAction(): bool
|
||||
{
|
||||
return match ($this) {
|
||||
self::LOW, self::MEDIUM => false,
|
||||
self::HIGH, self::CRITICAL => true,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get numeric priority for sorting
|
||||
*/
|
||||
public function getPriority(): int
|
||||
{
|
||||
return match ($this) {
|
||||
self::LOW => 1,
|
||||
self::MEDIUM => 2,
|
||||
self::HIGH => 3,
|
||||
self::CRITICAL => 4,
|
||||
};
|
||||
}
|
||||
}
|
||||
313
src/Framework/Config/WafConfig.php
Normal file
313
src/Framework/Config/WafConfig.php
Normal file
@@ -0,0 +1,313 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Config;
|
||||
|
||||
use App\Framework\Core\ValueObjects\Duration;
|
||||
use App\Framework\DDoS\DDoSConfig;
|
||||
use App\Framework\Waf\ValueObjects\LayerConfig;
|
||||
|
||||
/**
|
||||
* Web Application Firewall Configuration
|
||||
*
|
||||
* Central configuration for all WAF components including DDoS protection,
|
||||
* bot detection, input validation, and layer management.
|
||||
*/
|
||||
final readonly class WafConfig
|
||||
{
|
||||
public Duration $globalTimeout;
|
||||
|
||||
public function __construct(
|
||||
?Duration $globalTimeout = null,
|
||||
public bool $enabled = true,
|
||||
public bool $blockingMode = true,
|
||||
public bool $logDetections = true,
|
||||
public bool $enableDdosProtection = true,
|
||||
public bool $enableBotProtection = true,
|
||||
public bool $enableInputValidation = true,
|
||||
public bool $enableRateLimiting = true,
|
||||
public int $maxLayersPerRequest = 10,
|
||||
public float $globalConfidenceThreshold = 70.0,
|
||||
/** @var string[] */
|
||||
public array $trustedIps = ['127.0.0.1', '::1'],
|
||||
/** @var string[] */
|
||||
public array $exemptPaths = ['/health', '/ping', '/metrics'],
|
||||
/** @var string[] */
|
||||
public array $enabledLayers = [
|
||||
'input_validation',
|
||||
'sql_injection',
|
||||
'xss_protection',
|
||||
'bot_protection',
|
||||
'ddos_protection',
|
||||
'rate_limiting',
|
||||
'geo_blocking',
|
||||
]
|
||||
) {
|
||||
$this->globalTimeout = $globalTimeout ?? Duration::fromMilliseconds(200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create production configuration (strict security)
|
||||
*/
|
||||
public static function production(): self
|
||||
{
|
||||
return new self(
|
||||
globalTimeout : Duration::fromMilliseconds(200),
|
||||
enabled : true,
|
||||
blockingMode : true,
|
||||
logDetections : true,
|
||||
enableDdosProtection : true,
|
||||
enableBotProtection : true,
|
||||
enableInputValidation : true,
|
||||
enableRateLimiting : true,
|
||||
maxLayersPerRequest : 8, // Stricter timeout for production
|
||||
globalConfidenceThreshold: 85.0,
|
||||
trustedIps : [], // No trusted IPs in production
|
||||
exemptPaths : ['/health'], // Minimal exempt paths
|
||||
enabledLayers : [
|
||||
'input_validation',
|
||||
'sql_injection',
|
||||
'xss_protection',
|
||||
'bot_protection',
|
||||
'ddos_protection',
|
||||
'rate_limiting',
|
||||
'geo_blocking',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create development configuration (permissive)
|
||||
*/
|
||||
public static function development(): self
|
||||
{
|
||||
return new self(
|
||||
globalTimeout : Duration::fromMilliseconds(1000),
|
||||
enabled : true, // Log only in development
|
||||
blockingMode : false,
|
||||
logDetections : true, // Disable DDoS protection in dev
|
||||
enableDdosProtection : false, // Allow development tools
|
||||
enableBotProtection : false,
|
||||
enableInputValidation : true, // No rate limiting in dev
|
||||
enableRateLimiting : false,
|
||||
maxLayersPerRequest : 15, // More lenient for development
|
||||
globalConfidenceThreshold: 50.0,
|
||||
trustedIps : ['127.0.0.1', '::1', '192.168.0.0/16', '10.0.0.0/8'],
|
||||
exemptPaths : ['/health', '/ping', '/metrics', '/debug', '/api/test'],
|
||||
enabledLayers : [
|
||||
'input_validation',
|
||||
'sql_injection',
|
||||
'xss_protection',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create testing configuration (minimal protection)
|
||||
*/
|
||||
public static function testing(): self
|
||||
{
|
||||
return new self(
|
||||
globalTimeout : Duration::fromMilliseconds(5000), // Disable WAF in tests
|
||||
enabled : false,
|
||||
blockingMode : false,
|
||||
logDetections : false,
|
||||
enableDdosProtection : false,
|
||||
enableBotProtection : false,
|
||||
enableInputValidation : false,
|
||||
enableRateLimiting : false,
|
||||
maxLayersPerRequest : 5, // Very lenient for testing
|
||||
globalConfidenceThreshold: 90.0,
|
||||
trustedIps : ['127.0.0.1', '::1'],
|
||||
exemptPaths : ['*'], // Exempt all paths in testing
|
||||
enabledLayers : []
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get DDoS configuration environment type
|
||||
*
|
||||
* Returns a string indicating which DDoSConfig environment to use,
|
||||
* breaking the direct dependency on DDoSConfig class
|
||||
*/
|
||||
public function getDdosConfigEnvironment(): string
|
||||
{
|
||||
if (! $this->enableDdosProtection) {
|
||||
return 'testing'; // Disabled configuration
|
||||
}
|
||||
|
||||
// Determine environment based on configuration
|
||||
if ($this->blockingMode && $this->globalConfidenceThreshold >= 80.0) {
|
||||
return 'production';
|
||||
}
|
||||
|
||||
return 'development';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get DDoS configuration
|
||||
*
|
||||
* @deprecated Use getDdosConfigEnvironment() instead to avoid cyclic dependencies
|
||||
*/
|
||||
public function getDdosConfig(): DDoSConfig
|
||||
{
|
||||
$environment = $this->getDdosConfigEnvironment();
|
||||
|
||||
return match($environment) {
|
||||
'production' => DDoSConfig::production(),
|
||||
'testing' => DDoSConfig::testing(),
|
||||
default => DDoSConfig::development()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default layer configuration
|
||||
*/
|
||||
public function getDefaultLayerConfig(): LayerConfig
|
||||
{
|
||||
return match (true) {
|
||||
$this->blockingMode && $this->globalConfidenceThreshold >= 80.0 => LayerConfig::production(),
|
||||
! $this->blockingMode => LayerConfig::development(),
|
||||
default => LayerConfig::default()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if layer is enabled
|
||||
*/
|
||||
public function isLayerEnabled(string $layerName): bool
|
||||
{
|
||||
return $this->enabled && in_array($layerName, $this->enabledLayers, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if IP is trusted
|
||||
*/
|
||||
public function isTrustedIp(string $ip): bool
|
||||
{
|
||||
foreach ($this->trustedIps as $trustedIp) {
|
||||
if ($this->ipMatches($ip, $trustedIp)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if path is exempt
|
||||
*/
|
||||
public function isExemptPath(string $path): bool
|
||||
{
|
||||
foreach ($this->exemptPaths as $exemptPath) {
|
||||
if ($exemptPath === '*' || str_starts_with($path, $exemptPath)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if IP matches pattern (supports CIDR)
|
||||
*/
|
||||
private function ipMatches(string $ip, string $pattern): bool
|
||||
{
|
||||
// Exact match
|
||||
if ($ip === $pattern) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// CIDR match
|
||||
if (str_contains($pattern, '/')) {
|
||||
[$network, $maskBits] = explode('/', $pattern, 2);
|
||||
$maskBits = (int) $maskBits;
|
||||
|
||||
// IPv4 CIDR
|
||||
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) &&
|
||||
filter_var($network, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||||
$ipLong = ip2long($ip);
|
||||
$networkLong = ip2long($network);
|
||||
$mask = -1 << (32 - $maskBits);
|
||||
|
||||
return ($ipLong & $mask) === ($networkLong & $mask);
|
||||
}
|
||||
|
||||
// IPv6 CIDR (simplified)
|
||||
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) &&
|
||||
filter_var($network, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
|
||||
// For IPv6, we'd need more complex subnet matching
|
||||
// For now, just do exact match for IPv6
|
||||
return $ip === $network;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get configuration for environment detection
|
||||
*/
|
||||
public function getEnvironmentType(): string
|
||||
{
|
||||
return match (true) {
|
||||
! $this->enabled => 'testing',
|
||||
! $this->blockingMode => 'development',
|
||||
$this->globalConfidenceThreshold >= 80.0 => 'production',
|
||||
default => 'development'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance with updated global timeout
|
||||
*/
|
||||
public function withGlobalTimeout(Duration $globalTimeout): self
|
||||
{
|
||||
return new self(
|
||||
globalTimeout : $globalTimeout,
|
||||
enabled : $this->enabled,
|
||||
blockingMode : $this->blockingMode,
|
||||
logDetections : $this->logDetections,
|
||||
enableDdosProtection : $this->enableDdosProtection,
|
||||
enableBotProtection : $this->enableBotProtection,
|
||||
enableInputValidation : $this->enableInputValidation,
|
||||
enableRateLimiting : $this->enableRateLimiting,
|
||||
maxLayersPerRequest : $this->maxLayersPerRequest,
|
||||
globalConfidenceThreshold: $this->globalConfidenceThreshold,
|
||||
trustedIps : $this->trustedIps,
|
||||
exemptPaths : $this->exemptPaths,
|
||||
enabledLayers : $this->enabledLayers
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert to array for serialization
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'enabled' => $this->enabled,
|
||||
'blocking_mode' => $this->blockingMode,
|
||||
'log_detections' => $this->logDetections,
|
||||
'environment' => $this->getEnvironmentType(),
|
||||
'components' => [
|
||||
'ddos_protection' => $this->enableDdosProtection,
|
||||
'bot_protection' => $this->enableBotProtection,
|
||||
'input_validation' => $this->enableInputValidation,
|
||||
'rate_limiting' => $this->enableRateLimiting,
|
||||
],
|
||||
'limits' => [
|
||||
'max_layers_per_request' => $this->maxLayersPerRequest,
|
||||
'global_confidence_threshold' => $this->globalConfidenceThreshold,
|
||||
'global_timeout_ms' => $this->globalTimeout->toMilliseconds(),
|
||||
],
|
||||
'security' => [
|
||||
'trusted_ips_count' => count($this->trustedIps),
|
||||
'exempt_paths_count' => count($this->exemptPaths),
|
||||
'enabled_layers_count' => count($this->enabledLayers),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user