mirror of
https://github.com/ivuorinen/monolog-gdpr-filter.git
synced 2026-02-09 03:48:19 +00:00
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:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user