Files
monolog-gdpr-filter/src/RateLimitedAuditLogger.php
Ismo Vuorinen 8866daaf33 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
2025-12-22 13:38:18 +02:00

204 lines
7.0 KiB
PHP

<?php
declare(strict_types=1);
namespace Ivuorinen\MonologGdprFilter;
/**
* Rate-limited wrapper for audit logging to prevent log flooding.
*
* This class wraps any audit logger callable and applies rate limiting
* to prevent overwhelming the audit system with too many log entries.
*
* @api
*/
class RateLimitedAuditLogger
{
private readonly RateLimiter $rateLimiter;
/**
* @param callable(string,mixed,mixed):void $auditLogger The underlying audit logger
* @param int $maxRequestsPerMinute Maximum audit log entries per minute (default: 100)
* @param int $windowSeconds Time window for rate limiting in seconds (default: 60)
*/
public function __construct(
private readonly mixed $auditLogger,
int $maxRequestsPerMinute = 100,
int $windowSeconds = 60
) {
$this->rateLimiter = new RateLimiter($maxRequestsPerMinute, $windowSeconds);
}
/**
* Log an audit entry if rate limiting allows it.
*
* @param string $path The path or operation being audited
* @param mixed $original The original value
* @param mixed $masked The masked value
*/
public function __invoke(string $path, mixed $original, mixed $masked): void
{
// Use a combination of path and operation type as the rate limiting key
$key = $this->generateRateLimitKey($path);
if ($this->rateLimiter->isAllowed($key)) {
// Rate limit allows this log entry
/** @psalm-suppress RedundantConditionGivenDocblockType - Runtime validation for defensive programming */
if (is_callable($this->auditLogger)) {
($this->auditLogger)($path, $original, $masked);
}
} else {
// Rate limit exceeded - optionally log a rate limit warning
$this->logRateLimitExceeded($path, $key);
}
}
public function isOperationAllowed(string $path): bool
{
// Use a combination of path and operation type as the rate limiting key
$key = $this->generateRateLimitKey($path);
return $this->rateLimiter->isAllowed($key);
}
/**
* Get rate limiting statistics for all active operation types.
*
* @return int[][]
*
* @psalm-return array{
* 'audit:general_operations'?: array{
* current_requests: int<1, max>,
* remaining_requests: int<0, max>,
* time_until_reset: int<0, max>
* },
* 'audit:error_operations'?: array{
* current_requests: int<1, max>,
* remaining_requests: int<0, max>,
* time_until_reset: int<0, max>
* },
* 'audit:regex_operations'?: array{
* current_requests: int<1, max>,
* remaining_requests: int<0, max>,
* time_until_reset: int<0, max>
* },
* 'audit:conditional_operations'?: array{
* current_requests: int<1, max>,
* remaining_requests: int<0, max>,
* time_until_reset: int<0, max>
* },
* 'audit:json_operations'?: array{
* current_requests: int<1, max>,
* remaining_requests: int<0, max>,
* time_until_reset: int<0, max>
* }
* }
*/
public function getRateLimitStats(): array
{
// Get all possible operation types based on the classification logic
$operationTypes = [
'audit:json_operations',
'audit:conditional_operations',
'audit:regex_operations',
'audit:error_operations',
'audit:general_operations'
];
$stats = [];
foreach ($operationTypes as $type) {
$typeStats = $this->rateLimiter->getStats($type);
// Only include operation types that have been used
if ($typeStats['current_requests'] > 0) {
$stats[$type] = $typeStats;
}
}
return $stats;
}
/**
* Clear all rate limiting data.
*/
public function clearRateLimitData(): void
{
RateLimiter::clearAll();
}
/**
* Generate a rate limiting key based on the audit operation.
*
* This allows different types of operations to have separate rate limits.
*/
private function generateRateLimitKey(string $path): string
{
// Group similar operations together to prevent flooding of specific operation types
$operationType = $this->getOperationType($path);
// Use operation type as the primary key for rate limiting
return 'audit:' . $operationType;
}
/**
* Determine the operation type from the path.
*/
private function getOperationType(string $path): string
{
// Group different operations into categories for rate limiting
return match (true) {
str_contains($path, 'json_') => 'json_operations',
str_contains($path, 'conditional_') => 'conditional_operations',
str_contains($path, 'regex_') => 'regex_operations',
str_contains($path, 'preg_replace_') => 'regex_operations',
str_contains($path, 'error') => 'error_operations',
default => 'general_operations'
};
}
/**
* Log when rate limiting is exceeded (with its own rate limiting to prevent spam).
*/
private function logRateLimitExceeded(string $path, string $key): void
{
// Create a separate rate limiter for warnings to avoid interfering with main rate limiting
static $warningRateLimiter = null;
if ($warningRateLimiter === null) {
$warningRateLimiter = new RateLimiter(1, 60); // 1 warning per minute per operation type
}
$warningKey = 'warning:' . $key;
// Only log rate limit warnings once per minute per operation type to prevent warning spam
/** @psalm-suppress RedundantConditionGivenDocblockType - Runtime validation for defensive programming */
if ($warningRateLimiter->isAllowed($warningKey) === true && is_callable($this->auditLogger)) {
$statsJson = json_encode($this->rateLimiter->getStats($key));
($this->auditLogger)(
'rate_limit_exceeded',
$path,
sprintf(
'Audit logging rate limit exceeded for operation type: %s. Stats: %s',
$key,
$statsJson !== false ? $statsJson : 'N/A'
)
);
}
}
/**
* Create a factory method for common configurations.
*
* @psalm-param callable(string, mixed, mixed):void $auditLogger
*/
public static function create(
callable $auditLogger,
string $profile = 'default'
): self {
return match ($profile) {
'strict' => new self($auditLogger, 50, 60), // 50 per minute
'relaxed' => new self($auditLogger, 200, 60), // 200 per minute
'testing' => new self($auditLogger, 1000, 60), // 1000 per minute for testing
default => new self($auditLogger, 100, 60), // 100 per minute (default)
};
}
}