mirror of
https://github.com/ivuorinen/monolog-gdpr-filter.git
synced 2026-02-02 13:45:06 +00:00
* 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
227 lines
6.8 KiB
PHP
227 lines
6.8 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Ivuorinen\MonologGdprFilter;
|
|
|
|
use Ivuorinen\MonologGdprFilter\Exceptions\InvalidConfigurationException;
|
|
use Ivuorinen\MonologGdprFilter\Exceptions\InvalidRegexPatternException;
|
|
use Ivuorinen\MonologGdprFilter\MaskConstants as Mask;
|
|
|
|
/**
|
|
* FieldMaskConfig: configuration for masking/removal per field path.
|
|
*
|
|
* @api
|
|
*/
|
|
final readonly class FieldMaskConfig
|
|
{
|
|
public const MASK_REGEX = 'mask_regex';
|
|
|
|
public const REMOVE = 'remove';
|
|
|
|
public const REPLACE = 'replace';
|
|
|
|
public function __construct(
|
|
public string $type,
|
|
public ?string $replacement = null
|
|
) {}
|
|
|
|
/**
|
|
* Create a configuration for field removal.
|
|
*/
|
|
public static function remove(): self
|
|
{
|
|
return new self(self::REMOVE);
|
|
}
|
|
|
|
/**
|
|
* Create a configuration for static replacement.
|
|
*
|
|
* @param string $replacement The replacement value
|
|
*/
|
|
public static function replace(string $replacement): self
|
|
{
|
|
return new self(self::REPLACE, $replacement);
|
|
}
|
|
|
|
/**
|
|
* Create a configuration that uses the processor's global regex patterns.
|
|
* This is a shorthand for indicating "apply regex masking from the processor".
|
|
*/
|
|
public static function useProcessorPatterns(): self
|
|
{
|
|
return new self(self::MASK_REGEX);
|
|
}
|
|
|
|
/**
|
|
* Create a configuration for regex-based masking.
|
|
*
|
|
* @param string $pattern The regex pattern
|
|
* @param string $replacement The replacement string (default: '***MASKED***')
|
|
*
|
|
* @throws InvalidConfigurationException|InvalidRegexPatternException When pattern
|
|
* is empty or invalid, or replacement is empty
|
|
*/
|
|
public static function regexMask(string $pattern, string $replacement = Mask::MASK_MASKED): self
|
|
{
|
|
// Validate pattern is not empty
|
|
if (trim($pattern) === '') {
|
|
throw InvalidConfigurationException::emptyValue('regex pattern');
|
|
}
|
|
|
|
// Validate replacement is not empty
|
|
if (trim($replacement) === '') {
|
|
throw InvalidConfigurationException::emptyValue('replacement string');
|
|
}
|
|
|
|
// Validate regex pattern syntax
|
|
if (!self::isValidRegexPattern($pattern)) {
|
|
throw InvalidRegexPatternException::forPattern($pattern, 'Invalid regex pattern syntax');
|
|
}
|
|
|
|
return new self(self::MASK_REGEX, $pattern . '::' . $replacement);
|
|
}
|
|
|
|
/**
|
|
* Check if this configuration should remove the field.
|
|
*/
|
|
public function shouldRemove(): bool
|
|
{
|
|
return $this->type === self::REMOVE;
|
|
}
|
|
|
|
/**
|
|
* Check if this configuration has a regex pattern.
|
|
*/
|
|
public function hasRegexPattern(): bool
|
|
{
|
|
return $this->type === self::MASK_REGEX;
|
|
}
|
|
|
|
/**
|
|
* Get the regex pattern from a regex mask configuration.
|
|
*
|
|
* @return string|null The regex pattern or null if not a regex mask
|
|
*/
|
|
public function getRegexPattern(): ?string
|
|
{
|
|
if ($this->type !== self::MASK_REGEX || $this->replacement === null) {
|
|
return null;
|
|
}
|
|
|
|
$parts = explode('::', $this->replacement, 2);
|
|
return $parts[0] ?? null;
|
|
}
|
|
|
|
/**
|
|
* Get the replacement value.
|
|
*
|
|
* @return string|null The replacement value
|
|
*/
|
|
public function getReplacement(): ?string
|
|
{
|
|
if ($this->type === self::MASK_REGEX && $this->replacement !== null) {
|
|
$parts = explode('::', $this->replacement, 2);
|
|
return $parts[1] ?? Mask::MASK_MASKED;
|
|
}
|
|
|
|
return $this->replacement;
|
|
}
|
|
|
|
/**
|
|
* Convert to array representation.
|
|
*
|
|
* @return (null|string)[]
|
|
*
|
|
* @psalm-return array{type: string, replacement: null|string}
|
|
*/
|
|
public function toArray(): array
|
|
{
|
|
return [
|
|
'type' => $this->type,
|
|
'replacement' => $this->replacement,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Create from array representation.
|
|
*
|
|
* @param array<string, mixed> $data
|
|
*
|
|
* @throws InvalidConfigurationException|InvalidRegexPatternException When data contains invalid values
|
|
*/
|
|
public static function fromArray(array $data): self
|
|
{
|
|
$type = $data['type'] ?? self::REPLACE;
|
|
$replacement = $data['replacement'] ?? null;
|
|
|
|
// Validate type
|
|
$validTypes = [self::MASK_REGEX, self::REMOVE, self::REPLACE];
|
|
if (!in_array($type, $validTypes, true)) {
|
|
$validList = implode(', ', $validTypes);
|
|
throw InvalidConfigurationException::forParameter(
|
|
'type',
|
|
$type,
|
|
sprintf("Must be one of: %s", $validList)
|
|
);
|
|
}
|
|
|
|
// Validate replacement for REPLACE type - only when explicitly provided
|
|
if (
|
|
$type === self::REPLACE &&
|
|
array_key_exists('replacement', $data) &&
|
|
($replacement === null || trim($replacement) === '')
|
|
) {
|
|
throw InvalidConfigurationException::forParameter(
|
|
'replacement',
|
|
null,
|
|
'Cannot be null or empty for REPLACE type'
|
|
);
|
|
}
|
|
|
|
return new self($type, $replacement);
|
|
}
|
|
|
|
/**
|
|
* Validate if a regex pattern is syntactically correct.
|
|
*
|
|
* @param string $pattern The regex pattern to validate
|
|
* @return bool True if valid, false otherwise
|
|
*/
|
|
private static function isValidRegexPattern(string $pattern): bool
|
|
{
|
|
// Suppress warnings for invalid patterns
|
|
$previousErrorReporting = error_reporting(E_ERROR);
|
|
|
|
try {
|
|
// Test the pattern by attempting to use it
|
|
/** @psalm-suppress ArgumentTypeCoercion - Pattern validated by caller */
|
|
$result = @preg_match($pattern, '');
|
|
|
|
// Check if preg_match succeeded (returns 0 or 1) or failed (returns false)
|
|
$isValid = $result !== false;
|
|
|
|
// Additional check for PREG errors
|
|
if ($isValid && preg_last_error() !== PREG_NO_ERROR) {
|
|
$isValid = false;
|
|
}
|
|
|
|
// Additional validation for effectively empty patterns
|
|
// Check for patterns that are effectively empty (like '//' or '/\s*/')
|
|
// Extract the pattern content between delimiters
|
|
if ($isValid && preg_match('/^(.)(.*?)\1[gimuxXs]*$/', $pattern, $matches)) {
|
|
$patternContent = $matches[2];
|
|
// Reject patterns that are empty or only whitespace-based
|
|
if ($patternContent === '' || trim($patternContent) === '' || $patternContent === '\s*') {
|
|
$isValid = false;
|
|
}
|
|
}
|
|
|
|
return $isValid;
|
|
} finally {
|
|
// Restore previous error reporting level
|
|
error_reporting($previousErrorReporting);
|
|
}
|
|
}
|
|
}
|