feat: add advanced architecture, documentation, and coverage improvements (#65)

* fix(style): resolve PHPCS line-length warnings in source files

* fix(style): resolve PHPCS line-length warnings in test files

* feat(audit): add structured audit logging with ErrorContext and AuditContext

- ErrorContext: standardized error information with sensitive data sanitization
- AuditContext: structured context for audit entries with operation types
- StructuredAuditLogger: enhanced audit logger wrapper with timing support

* feat(recovery): add recovery mechanism for failed masking operations

- FailureMode enum: FAIL_OPEN, FAIL_CLOSED, FAIL_SAFE modes
- RecoveryStrategy interface and RecoveryResult value object
- RetryStrategy: exponential backoff with configurable attempts
- FallbackMaskStrategy: type-aware fallback values

* feat(strategies): add CallbackMaskingStrategy for custom masking logic

- Wraps custom callbacks as MaskingStrategy implementations
- Factory methods: constant(), hash(), partial() for common use cases
- Supports exact match and prefix match for field paths

* docs: add framework integration guides and examples

- symfony-integration.md: Symfony service configuration and Monolog setup
- psr3-decorator.md: PSR-3 logger decorator pattern implementation
- framework-examples.md: CakePHP, CodeIgniter 4, Laminas, Yii2, PSR-15
- docker-development.md: Docker development environment guide

* chore(docker): add Docker development environment

- Dockerfile: PHP 8.2-cli-alpine with Xdebug for coverage
- docker-compose.yml: development services with volume mounts

* feat(demo): add interactive GDPR pattern tester playground

- PatternTester.php: pattern testing utility with strategy support
- index.php: web API endpoint with JSON response handling
- playground.html: interactive web interface for testing patterns

* docs(todo): update with completed medium priority items

- Mark all PHPCS warnings as fixed (81 → 0)
- Document new Audit and Recovery features
- Update test count to 1,068 tests with 2,953 assertions
- Move remaining items to low priority

* feat: add advanced architecture, documentation, and coverage improvements

- Add architecture improvements:
  - ArrayAccessorInterface and DotArrayAccessor for decoupled array access
  - MaskingOrchestrator for single-responsibility masking coordination
  - GdprProcessorBuilder for fluent configuration
  - MaskingPluginInterface and AbstractMaskingPlugin for plugin architecture
  - PluginAwareProcessor for plugin hook execution
  - AuditLoggerFactory for instance-based audit logger creation

- Add advanced features:
  - SerializedDataProcessor for handling print_r/var_export/serialize output
  - KAnonymizer with GeneralizationStrategy for GDPR k-anonymity
  - RetentionPolicy for configurable data retention periods
  - StreamingProcessor for memory-efficient large log processing

- Add comprehensive documentation:
  - docs/performance-tuning.md - benchmarking, optimization, caching
  - docs/troubleshooting.md - common issues and solutions
  - docs/logging-integrations.md - ELK, Graylog, Datadog, etc.
  - docs/plugin-development.md - complete plugin development guide

- Improve test coverage (84.41% → 85.07%):
  - ConditionalRuleFactoryInstanceTest (100% coverage)
  - GdprProcessorBuilderEdgeCasesTest (100% coverage)
  - StrategyEdgeCasesTest for ReDoS detection and type parsing
  - 78 new tests, 119 new assertions

- Update TODO.md with current statistics:
  - 141 PHP files, 1,346 tests, 85.07% line coverage

* chore: tests, update actions, sonarcloud issues

* chore: rector

* fix: more sonarcloud fixes

* chore: more fixes

* refactor: copilot review fix

* chore: rector
This commit is contained in:
2025-12-22 13:38:18 +02:00
committed by GitHub
parent b1eb567b92
commit 8866daaf33
112 changed files with 15391 additions and 607 deletions

View File

@@ -101,7 +101,36 @@ class SecuritySanitizerTest extends TestCase
/**
* @return string[][]
*
* @psalm-return array{'password with equals': array{input: 'Error: password=secretpass', shouldNotContain: 'secretpass'}, 'api key with underscore': array{input: 'Failed: api_key=key123456', shouldNotContain: 'key123456'}, 'api key with dash': array{input: 'Failed: api-key: key123456', shouldNotContain: 'key123456'}, 'token in header': array{input: 'Request failed: Authorization: Bearer token123abc', shouldNotContain: 'token123abc'}, 'mysql connection string': array{input: 'DB error: mysql://user:pass@localhost:3306', shouldNotContain: 'user:pass'}, 'secret key': array{input: 'Config: secret_key=my-secret-123', shouldNotContain: 'my-secret-123'}, 'private key': array{input: 'Error: private_key=pk_test_12345', shouldNotContain: 'pk_test_12345'}}
* @psalm-return array{
* 'password with equals': array{
* input: 'Error: password=secretpass',
* shouldNotContain: 'secretpass'
* },
* 'api key with underscore': array{
* input: 'Failed: api_key=key123456',
* shouldNotContain: 'key123456'
* },
* 'api key with dash': array{
* input: 'Failed: api-key: key123456',
* shouldNotContain: 'key123456'
* },
* 'token in header': array{
* input: 'Request failed: Authorization: Bearer token123abc',
* shouldNotContain: 'token123abc'
* },
* 'mysql connection string': array{
* input: 'DB error: mysql://user:pass@localhost:3306',
* shouldNotContain: 'user:pass'
* },
* 'secret key': array{
* input: 'Config: secret_key=my-secret-123',
* shouldNotContain: 'my-secret-123'
* },
* 'private key': array{
* input: 'Error: private_key=pk_test_12345',
* shouldNotContain: 'pk_test_12345'
* }
* }
*/
public static function sensitivePatternProvider(): array
{
@@ -167,4 +196,192 @@ class SecuritySanitizerTest extends TestCase
$this->assertStringContainsString(TestConstants::ERROR_TRUNCATED_SECURITY, $sanitized);
$this->assertLessThanOrEqual(550, strlen($sanitized)); // 500 + truncation message
}
#[Test]
public function sanitizesPwdAndPassVariations(): void
{
$message = 'Connection failed: pwd=mypassword pass=anotherpass';
$sanitized = SecuritySanitizer::sanitizeErrorMessage($message);
$this->assertStringContainsString('pwd=***', $sanitized);
$this->assertStringContainsString('pass=***', $sanitized);
$this->assertStringNotContainsString('mypassword', $sanitized);
$this->assertStringNotContainsString('anotherpass', $sanitized);
}
#[Test]
public function sanitizesHostServerHostnamePatterns(): void
{
$message = 'Error: host=db.example.com server=192.168.1.1 hostname=internal.local';
$sanitized = SecuritySanitizer::sanitizeErrorMessage($message);
$this->assertStringContainsString('host=***', $sanitized);
$this->assertStringContainsString('server=***', $sanitized);
$this->assertStringContainsString('hostname=***', $sanitized);
$this->assertStringNotContainsString('db.example.com', $sanitized);
$this->assertStringNotContainsString('internal.local', $sanitized);
}
#[Test]
public function sanitizesUsernameAndUidPatterns(): void
{
$message = 'Auth error: username=admin uid=12345';
$sanitized = SecuritySanitizer::sanitizeErrorMessage($message);
$this->assertStringContainsString('username=***', $sanitized);
$this->assertStringContainsString('uid=***', $sanitized);
$this->assertStringNotContainsString('=admin', $sanitized);
}
#[Test]
public function sanitizesStripeStyleKeys(): void
{
$message = 'Stripe error: sk_live_abc123def456 pk_test_xyz789ghi012';
$sanitized = SecuritySanitizer::sanitizeErrorMessage($message);
$this->assertStringContainsString('sk_***', $sanitized);
$this->assertStringContainsString('pk_***', $sanitized);
$this->assertStringNotContainsString('sk_live_abc123def456', $sanitized);
$this->assertStringNotContainsString('pk_test_xyz789ghi012', $sanitized);
}
#[Test]
public function sanitizesRedisConnectionString(): void
{
$message = 'Redis error: redis://user:pass@localhost:6379';
$sanitized = SecuritySanitizer::sanitizeErrorMessage($message);
$this->assertStringContainsString('redis://***:***@***:***', $sanitized);
$this->assertStringNotContainsString('user:pass@', $sanitized);
}
#[Test]
public function sanitizesPostgresqlConnectionString(): void
{
$message = 'Connection error: postgresql://admin:password@pg.server:5432';
$sanitized = SecuritySanitizer::sanitizeErrorMessage($message);
$this->assertStringContainsString('postgresql://***:***@***:***', $sanitized);
$this->assertStringNotContainsString('admin:password@', $sanitized);
}
#[Test]
public function sanitizesJwtSecretPatterns(): void
{
$message = 'JWT error: jwt_secret=mysupersecret123';
$sanitized = SecuritySanitizer::sanitizeErrorMessage($message);
$this->assertStringContainsString('jwt_secret=***', $sanitized);
$this->assertStringNotContainsString('mysupersecret123', $sanitized);
}
#[Test]
public function sanitizesSuperSecretPattern(): void
{
$message = 'Config contains super_secret_value';
$sanitized = SecuritySanitizer::sanitizeErrorMessage($message);
$this->assertStringContainsString(MaskConstants::MASK_SECRET, $sanitized);
$this->assertStringNotContainsString('super_secret_value', $sanitized);
}
#[Test]
#[DataProvider('internalIpRangesProvider')]
public function sanitizesInternalIpAddresses(string $ip, string $range): void
{
$message = 'Cannot reach server at ' . $ip;
$sanitized = SecuritySanitizer::sanitizeErrorMessage($message);
$this->assertStringNotContainsString($ip, $sanitized, "Internal IP in $range range should be masked");
$this->assertStringContainsString('***.***.***', $sanitized);
}
/**
* @return array<string, array{ip: string, range: string}>
*/
public static function internalIpRangesProvider(): array
{
return [
'10.0.0.1' => ['ip' => '10.0.0.1', 'range' => '10.x.x.x'],
'10.255.255.255' => ['ip' => '10.255.255.255', 'range' => '10.x.x.x'],
'172.16.0.1' => ['ip' => '172.16.0.1', 'range' => '172.16-31.x.x'],
'172.31.255.255' => ['ip' => '172.31.255.255', 'range' => '172.16-31.x.x'],
'192.168.0.1' => ['ip' => '192.168.0.1', 'range' => '192.168.x.x'],
'192.168.255.255' => ['ip' => '192.168.255.255', 'range' => '192.168.x.x'],
];
}
#[Test]
public function preservesPublicIpAddresses(): void
{
$message = 'External server at 8.8.8.8 responded';
$sanitized = SecuritySanitizer::sanitizeErrorMessage($message);
$this->assertStringContainsString('8.8.8.8', $sanitized);
}
#[Test]
public function sanitizesSensitiveFilePaths(): void
{
$message = 'Error reading /var/www/config/secrets.json';
$sanitized = SecuritySanitizer::sanitizeErrorMessage($message);
$this->assertStringNotContainsString('/var/www/config', $sanitized);
$this->assertStringContainsString('/***/', $sanitized);
}
#[Test]
public function sanitizesWindowsSensitiveFilePaths(): void
{
$message = 'Error reading C:\\Users\\admin\\config\\secrets.txt';
$sanitized = SecuritySanitizer::sanitizeErrorMessage($message);
// Windows paths with sensitive keywords are masked
$this->assertStringNotContainsString('admin', $sanitized);
$this->assertStringNotContainsString('secrets.txt', $sanitized);
$this->assertStringContainsString('C:\\***', $sanitized);
}
#[Test]
public function sanitizesGenericSecretPatterns(): void
{
$message = 'Config: app_secret=abcd1234567890 encryption_key: xyz9876543210abc';
$sanitized = SecuritySanitizer::sanitizeErrorMessage($message);
$this->assertStringNotContainsString('abcd1234567890', $sanitized);
$this->assertStringNotContainsString('xyz9876543210abc', $sanitized);
}
#[Test]
public function classCannotBeInstantiated(): void
{
$reflection = new \ReflectionClass(SecuritySanitizer::class);
$constructor = $reflection->getConstructor();
$this->assertNotNull($constructor);
$this->assertTrue($constructor->isPrivate());
}
#[Test]
public function sanitizesUserPattern(): void
{
$message = 'Connection failed: user=dbadmin';
$sanitized = SecuritySanitizer::sanitizeErrorMessage($message);
$this->assertStringContainsString('user=***', $sanitized);
$this->assertStringNotContainsString('dbadmin', $sanitized);
}
#[Test]
public function caseInsensitivePatternMatching(): void
{
$message = 'Error: PASSWORD=secret HOST=server.local TOKEN=abc123token';
$sanitized = SecuritySanitizer::sanitizeErrorMessage($message);
// Case-insensitive matching means uppercase is matched but replaced with lowercase pattern
$this->assertStringNotContainsString('secret', $sanitized);
$this->assertStringNotContainsString('server.local', $sanitized);
$this->assertStringNotContainsString('abc123token', $sanitized);
$this->assertStringContainsString('=***', $sanitized);
}
}