mirror of
https://github.com/ivuorinen/monolog-gdpr-filter.git
synced 2026-03-13 10:01:20 +00:00
feat: performance, integrations, advanced features (#2)
* feat: performance, integrations, advanced features * chore: fix linting problems * chore: suppressions and linting * chore(lint): pre-commit linting, fixes * feat: comprehensive input validation, security hardening, and regression testing - Add extensive input validation throughout codebase with proper error handling - Implement comprehensive security hardening with ReDoS protection and bounds checking - Add 3 new regression test suites covering critical bugs, security, and validation scenarios - Enhance rate limiting with memory management and configurable cleanup intervals - Update configuration security settings and improve Laravel integration - Fix TODO.md timestamps to reflect actual development timeline - Strengthen static analysis configuration and improve code quality standards * feat: configure static analysis tools and enhance development workflow - Complete configuration of Psalm, PHPStan, and Rector for harmonious static analysis. - Fix invalid configurations and tool conflicts that prevented proper code quality analysis. - Add comprehensive safe analysis script with interactive workflow, backup/restore capabilities, and dry-run modes. Update documentation with linting policy requiring issue resolution over suppression. - Clean completed items from TODO to focus on actionable improvements. - All static analysis tools now work together seamlessly to provide code quality insights without breaking existing functionality. * fix(test): update Invalid regex pattern expectation * chore: phpstan, psalm fixes * chore: phpstan, psalm fixes, more tests * chore: tooling tweaks, cleanup * chore: tweaks to get the tests pass * fix(lint): rector config tweaks and successful run * feat: refactoring, more tests, fixes, cleanup * chore: deduplication, use constants * chore: psalm fixes * chore: ignore phpstan deliberate errors in tests * chore: improve codebase, deduplicate code * fix: lint * chore: deduplication, codebase simplification, sonarqube fixes * fix: resolve SonarQube reliability rating issues Fix useless object instantiation warnings in test files by assigning instantiated objects to variables. This resolves the SonarQube reliability rating issue (was C, now targeting A). Changes: - tests/Strategies/MaskingStrategiesTest.php: Fix 3 instances - tests/Strategies/FieldPathMaskingStrategyTest.php: Fix 1 instance The tests use expectException() to verify that constructors throw exceptions for invalid input. SonarQube flagged standalone `new` statements as useless. Fixed by assigning to variables with explicit unset() and fail() calls. All tests pass (623/623) and static analysis tools pass. * fix: resolve more SonarQube detected issues * fix: resolve psalm detected issues * fix: resolve more SonarQube detected issues * fix: resolve psalm detected issues * fix: duplications * fix: resolve SonarQube reliability rating issues * fix: resolve psalm and phpstan detected issues
This commit is contained in:
177
src/RateLimitedAuditLogger.php
Normal file
177
src/RateLimitedAuditLogger.php
Normal file
@@ -0,0 +1,177 @@
|
||||
<?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)
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user