mirror of
https://github.com/ivuorinen/monolog-gdpr-filter.git
synced 2026-01-26 11:44:04 +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:
293
demo/PatternTester.php
Normal file
293
demo/PatternTester.php
Normal file
@@ -0,0 +1,293 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Ivuorinen\MonologGdprFilter\Demo;
|
||||
|
||||
use Ivuorinen\MonologGdprFilter\GdprProcessor;
|
||||
use Ivuorinen\MonologGdprFilter\Strategies\RegexMaskingStrategy;
|
||||
use Ivuorinen\MonologGdprFilter\Strategies\FieldPathMaskingStrategy;
|
||||
use Ivuorinen\MonologGdprFilter\Strategies\StrategyManager;
|
||||
use Ivuorinen\MonologGdprFilter\FieldMaskConfig;
|
||||
use Ivuorinen\MonologGdprFilter\DefaultPatterns;
|
||||
use Monolog\Level;
|
||||
use Monolog\LogRecord;
|
||||
use DateTimeImmutable;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Pattern testing utility for the demo playground.
|
||||
*/
|
||||
final class PatternTester
|
||||
{
|
||||
/** @var array<array{path: string, original: mixed, masked: mixed}> */
|
||||
private array $auditLog = [];
|
||||
|
||||
/**
|
||||
* Test regex patterns against sample text.
|
||||
*
|
||||
* @param string $text Sample text to test
|
||||
* @param array<string, string> $patterns Regex patterns to apply
|
||||
* @return array{masked: string, matches: array<string, array<string>>, errors: array<string>}
|
||||
*/
|
||||
public function testPatterns(string $text, array $patterns): array
|
||||
{
|
||||
$errors = [];
|
||||
$matches = [];
|
||||
$masked = $text;
|
||||
|
||||
foreach ($patterns as $pattern => $replacement) {
|
||||
// Validate pattern
|
||||
if (@preg_match($pattern, '') === false) {
|
||||
$errors[] = "Invalid pattern: {$pattern}";
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find matches
|
||||
if (preg_match_all($pattern, $text, $found)) {
|
||||
$matches[$pattern] = $found[0];
|
||||
}
|
||||
|
||||
// Apply replacement
|
||||
$result = @preg_replace($pattern, $replacement, $masked);
|
||||
if ($result !== null) {
|
||||
$masked = $result;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'masked' => $masked,
|
||||
'matches' => $matches,
|
||||
'errors' => $errors,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test with the full GdprProcessor.
|
||||
*
|
||||
* @param string $message Log message to test
|
||||
* @param array<string, mixed> $context Log context to test
|
||||
* @param array<string, string> $patterns Custom patterns (or empty for defaults)
|
||||
* @param array<string, string|FieldMaskConfig> $fieldPaths Field path configurations
|
||||
* @return array{
|
||||
* original_message: string,
|
||||
* masked_message: string,
|
||||
* original_context: array<string, mixed>,
|
||||
* masked_context: array<string, mixed>,
|
||||
* audit_log: array<array{path: string, original: mixed, masked: mixed}>,
|
||||
* errors: array<string>
|
||||
* }
|
||||
*/
|
||||
public function testProcessor(
|
||||
string $message,
|
||||
array $context = [],
|
||||
array $patterns = [],
|
||||
array $fieldPaths = []
|
||||
): array {
|
||||
$this->auditLog = [];
|
||||
$errors = [];
|
||||
|
||||
try {
|
||||
// Use default patterns if none provided
|
||||
if (empty($patterns)) {
|
||||
$patterns = DefaultPatterns::get();
|
||||
}
|
||||
|
||||
// Create audit logger
|
||||
$auditLogger = function (string $path, mixed $original, mixed $masked): void {
|
||||
$this->auditLog[] = [
|
||||
'path' => $path,
|
||||
'original' => $original,
|
||||
'masked' => $masked,
|
||||
];
|
||||
};
|
||||
|
||||
// Convert field paths to FieldMaskConfig
|
||||
$configuredPaths = $this->convertFieldPathsToConfig($fieldPaths);
|
||||
|
||||
// Create processor
|
||||
$processor = new GdprProcessor(
|
||||
patterns: $patterns,
|
||||
fieldPaths: $configuredPaths,
|
||||
auditLogger: $auditLogger
|
||||
);
|
||||
|
||||
// Create log record
|
||||
$record = new LogRecord(
|
||||
datetime: new DateTimeImmutable(),
|
||||
channel: 'demo',
|
||||
level: Level::Info,
|
||||
message: $message,
|
||||
context: $context
|
||||
);
|
||||
|
||||
// Process
|
||||
$result = $processor($record);
|
||||
|
||||
return [
|
||||
'original_message' => $message,
|
||||
'masked_message' => $result->message,
|
||||
'original_context' => $context,
|
||||
'masked_context' => $result->context,
|
||||
'audit_log' => $this->auditLog,
|
||||
'errors' => $errors,
|
||||
];
|
||||
} catch (Throwable $e) {
|
||||
$errors[] = $e->getMessage();
|
||||
|
||||
return [
|
||||
'original_message' => $message,
|
||||
'masked_message' => $message,
|
||||
'original_context' => $context,
|
||||
'masked_context' => $context,
|
||||
'audit_log' => [],
|
||||
'errors' => $errors,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test with the Strategy pattern.
|
||||
*
|
||||
* @param string $message Log message
|
||||
* @param array<string, mixed> $context Log context
|
||||
* @param array<string, string> $patterns Regex patterns
|
||||
* @param array<string> $includePaths Paths to include
|
||||
* @param array<string> $excludePaths Paths to exclude
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function testStrategies(
|
||||
string $message,
|
||||
array $context = [],
|
||||
array $patterns = [],
|
||||
array $includePaths = [],
|
||||
array $excludePaths = []
|
||||
): array {
|
||||
$errors = [];
|
||||
|
||||
try {
|
||||
if (empty($patterns)) {
|
||||
$patterns = DefaultPatterns::get();
|
||||
}
|
||||
|
||||
// Create strategies
|
||||
$regexStrategy = new RegexMaskingStrategy(
|
||||
patterns: $patterns,
|
||||
includePaths: $includePaths,
|
||||
excludePaths: $excludePaths
|
||||
);
|
||||
|
||||
// Create strategy manager
|
||||
$manager = new StrategyManager([$regexStrategy]);
|
||||
|
||||
// Create log record
|
||||
$record = new LogRecord(
|
||||
datetime: new DateTimeImmutable(),
|
||||
channel: 'demo',
|
||||
level: Level::Info,
|
||||
message: $message,
|
||||
context: $context
|
||||
);
|
||||
|
||||
// Mask message
|
||||
$maskedMessage = $manager->maskValue($message, 'message', $record);
|
||||
|
||||
// Mask context recursively
|
||||
$maskedContext = $this->maskContextWithStrategies($context, $manager, $record);
|
||||
|
||||
return [
|
||||
'original_message' => $message,
|
||||
'masked_message' => $maskedMessage,
|
||||
'original_context' => $context,
|
||||
'masked_context' => $maskedContext,
|
||||
'strategy_stats' => $manager->getStatistics(),
|
||||
'errors' => $errors,
|
||||
];
|
||||
} catch (Throwable $e) {
|
||||
$errors[] = $e->getMessage();
|
||||
|
||||
return [
|
||||
'original_message' => $message,
|
||||
'masked_message' => $message,
|
||||
'original_context' => $context,
|
||||
'masked_context' => $context,
|
||||
'strategy_stats' => [],
|
||||
'errors' => $errors,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default patterns for display.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function getDefaultPatterns(): array
|
||||
{
|
||||
return DefaultPatterns::get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a single regex pattern.
|
||||
*
|
||||
* @return array{valid: bool, error: string|null}
|
||||
*/
|
||||
public function validatePattern(string $pattern): array
|
||||
{
|
||||
if ($pattern === '') {
|
||||
return ['valid' => false, 'error' => 'Pattern cannot be empty'];
|
||||
}
|
||||
|
||||
if (@preg_match($pattern, '') === false) {
|
||||
$error = preg_last_error_msg();
|
||||
return ['valid' => false, 'error' => $error];
|
||||
}
|
||||
|
||||
return ['valid' => true, 'error' => null];
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert field paths to configuration array.
|
||||
*
|
||||
* @param array<string, string|FieldMaskConfig> $fieldPaths
|
||||
* @return array<string, string|FieldMaskConfig>
|
||||
*/
|
||||
private function convertFieldPathsToConfig(array $fieldPaths): array
|
||||
{
|
||||
$configuredPaths = [];
|
||||
foreach ($fieldPaths as $path => $config) {
|
||||
// Accept both FieldMaskConfig instances and strings
|
||||
$configuredPaths[$path] = $config;
|
||||
}
|
||||
return $configuredPaths;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively mask context values using strategy manager.
|
||||
*
|
||||
* @param array<string, mixed> $context
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function maskContextWithStrategies(
|
||||
array $context,
|
||||
StrategyManager $manager,
|
||||
LogRecord $record,
|
||||
string $prefix = ''
|
||||
): array {
|
||||
$result = [];
|
||||
|
||||
foreach ($context as $key => $value) {
|
||||
$path = $prefix === '' ? $key : $prefix . '.' . $key;
|
||||
|
||||
if (is_array($value)) {
|
||||
$result[$key] = $this->maskContextWithStrategies($value, $manager, $record, $path);
|
||||
} elseif (is_string($value)) {
|
||||
$result[$key] = $manager->maskValue($value, $path, $record);
|
||||
} else {
|
||||
$result[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user