mirror of
https://github.com/ivuorinen/monolog-gdpr-filter.git
synced 2026-03-03 19:57:16 +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;
|
||||
}
|
||||
}
|
||||
270
demo/index.php
Normal file
270
demo/index.php
Normal file
@@ -0,0 +1,270 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* GDPR Pattern Tester - Interactive Demo
|
||||
*
|
||||
* This is a simple web interface for testing GDPR masking patterns.
|
||||
* Run with: php -S localhost:8080 demo/index.php
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
use Ivuorinen\MonologGdprFilter\Demo\PatternTester;
|
||||
|
||||
// Auto-load the PatternTester class
|
||||
spl_autoload_register(function (string $class): void {
|
||||
if (str_starts_with($class, 'Ivuorinen\\MonologGdprFilter\\Demo\\')) {
|
||||
$file = __DIR__ . '/' . substr($class, strlen('Ivuorinen\\MonologGdprFilter\\Demo\\')) . '.php';
|
||||
if (file_exists($file)) {
|
||||
require_once $file;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$tester = new PatternTester();
|
||||
|
||||
// Handle API requests
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_SERVER['CONTENT_TYPE']) && str_contains($_SERVER['CONTENT_TYPE'], 'application/json')) {
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
if (!is_array($input)) {
|
||||
echo json_encode(['error' => 'Invalid JSON input']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$action = $input['action'] ?? 'test';
|
||||
|
||||
$result = match ($action) {
|
||||
'test_patterns' => $tester->testPatterns(
|
||||
$input['text'] ?? '',
|
||||
$input['patterns'] ?? []
|
||||
),
|
||||
'test_processor' => $tester->testProcessor(
|
||||
$input['message'] ?? '',
|
||||
$input['context'] ?? [],
|
||||
$input['patterns'] ?? [],
|
||||
$input['field_paths'] ?? []
|
||||
),
|
||||
'test_strategies' => $tester->testStrategies(
|
||||
$input['message'] ?? '',
|
||||
$input['context'] ?? [],
|
||||
$input['patterns'] ?? []
|
||||
),
|
||||
'validate_pattern' => $tester->validatePattern($input['pattern'] ?? ''),
|
||||
'get_defaults' => ['patterns' => $tester->getDefaultPatterns()],
|
||||
default => ['error' => 'Unknown action'],
|
||||
};
|
||||
|
||||
echo json_encode($result, JSON_PRETTY_PRINT);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Serve the HTML template
|
||||
$templatePath = __DIR__ . '/templates/playground.html';
|
||||
if (file_exists($templatePath)) {
|
||||
readfile($templatePath);
|
||||
} else {
|
||||
// Fallback inline template
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>GDPR Pattern Tester</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
h1 { color: #333; }
|
||||
.container {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
}
|
||||
.panel {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
.full-width { grid-column: 1 / -1; }
|
||||
label { display: block; margin-bottom: 5px; font-weight: 600; }
|
||||
textarea, input[type="text"] {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
font-size: 14px;
|
||||
}
|
||||
textarea { min-height: 150px; resize: vertical; }
|
||||
button {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
button:hover { background: #0056b3; }
|
||||
.result {
|
||||
background: #f8f9fa;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
margin-top: 10px;
|
||||
font-family: monospace;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
.error { background: #f8d7da; color: #721c24; }
|
||||
.success { background: #d4edda; color: #155724; }
|
||||
.match { background: #fff3cd; padding: 2px 4px; border-radius: 2px; }
|
||||
.patterns-list {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
font-size: 12px;
|
||||
}
|
||||
.pattern-item {
|
||||
padding: 5px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
.pattern-item code {
|
||||
background: #e9ecef;
|
||||
padding: 2px 4px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>GDPR Pattern Tester</h1>
|
||||
<p>Test regex patterns for masking sensitive data in log messages.</p>
|
||||
|
||||
<div class="container">
|
||||
<div class="panel">
|
||||
<h2>Sample Text</h2>
|
||||
<label for="sampleText">Enter text containing sensitive data:</label>
|
||||
<textarea id="sampleText">User john.doe@example.com logged in from 192.168.1.100.
|
||||
Credit card: 4532-1234-5678-9012
|
||||
SSN: 123-45-6789
|
||||
Phone: +1 (555) 123-4567</textarea>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h2>Custom Patterns</h2>
|
||||
<label for="patterns">JSON patterns (pattern => replacement):</label>
|
||||
<textarea id="patterns">{
|
||||
"/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}/": "[EMAIL]",
|
||||
"/\\b\\d{3}-\\d{2}-\\d{4}\\b/": "***-**-****",
|
||||
"/\\b\\d{4}[\\s-]?\\d{4}[\\s-]?\\d{4}[\\s-]?\\d{4}\\b/": "****-****-****-****"
|
||||
}</textarea>
|
||||
<button onclick="loadDefaults()">Load Default Patterns</button>
|
||||
</div>
|
||||
|
||||
<div class="panel full-width">
|
||||
<button onclick="testPatterns()">Test Patterns</button>
|
||||
<button onclick="testProcessor()">Test Full Processor</button>
|
||||
</div>
|
||||
|
||||
<div class="panel full-width">
|
||||
<h2>Results</h2>
|
||||
<div id="results" class="result">Results will appear here...</div>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h2>Default Patterns</h2>
|
||||
<div id="defaultPatterns" class="patterns-list">Loading...</div>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h2>Audit Log</h2>
|
||||
<div id="auditLog" class="result">Audit log will appear here...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function api(action, data = {}) {
|
||||
const response = await fetch(window.location.href, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ action, ...data })
|
||||
});
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async function testPatterns() {
|
||||
const text = document.getElementById('sampleText').value;
|
||||
let patterns;
|
||||
try {
|
||||
patterns = JSON.parse(document.getElementById('patterns').value);
|
||||
} catch (e) {
|
||||
showResult({ error: 'Invalid JSON in patterns: ' + e.message });
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await api('test_patterns', { text, patterns });
|
||||
showResult(result);
|
||||
}
|
||||
|
||||
async function testProcessor() {
|
||||
const message = document.getElementById('sampleText').value;
|
||||
let patterns;
|
||||
try {
|
||||
patterns = JSON.parse(document.getElementById('patterns').value);
|
||||
} catch (e) {
|
||||
patterns = {};
|
||||
}
|
||||
|
||||
const result = await api('test_processor', { message, patterns });
|
||||
showResult(result);
|
||||
if (result.audit_log) {
|
||||
document.getElementById('auditLog').textContent =
|
||||
JSON.stringify(result.audit_log, null, 2);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDefaults() {
|
||||
const result = await api('get_defaults');
|
||||
if (result.patterns) {
|
||||
document.getElementById('patterns').value =
|
||||
JSON.stringify(result.patterns, null, 4);
|
||||
}
|
||||
}
|
||||
|
||||
function showResult(result) {
|
||||
const el = document.getElementById('results');
|
||||
if (result.error || (result.errors && result.errors.length)) {
|
||||
el.className = 'result error';
|
||||
} else {
|
||||
el.className = 'result success';
|
||||
}
|
||||
el.textContent = JSON.stringify(result, null, 2);
|
||||
}
|
||||
|
||||
// Load defaults on page load
|
||||
(async function() {
|
||||
const result = await api('get_defaults');
|
||||
if (result.patterns) {
|
||||
const container = document.getElementById('defaultPatterns');
|
||||
container.innerHTML = Object.entries(result.patterns)
|
||||
.map(([pattern, replacement]) =>
|
||||
`<div class="pattern-item"><code>${pattern}</code> → <code>${replacement}</code></div>`
|
||||
).join('');
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
<?php
|
||||
}
|
||||
478
demo/templates/playground.html
Normal file
478
demo/templates/playground.html
Normal file
@@ -0,0 +1,478 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>GDPR Pattern Tester - Monolog GDPR Filter</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
header {
|
||||
text-align: center;
|
||||
color: white;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
header h1 {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
header p {
|
||||
opacity: 0.9;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
.card {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
|
||||
padding: 25px;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
.card:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.card h2 {
|
||||
color: #333;
|
||||
margin-bottom: 15px;
|
||||
font-size: 1.3rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.card h2::before {
|
||||
content: '';
|
||||
width: 4px;
|
||||
height: 24px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 2px;
|
||||
}
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 600;
|
||||
color: #555;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
textarea, input[type="text"] {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 2px solid #e1e5eb;
|
||||
border-radius: 8px;
|
||||
font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
|
||||
font-size: 13px;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
textarea:focus, input[type="text"]:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||||
}
|
||||
textarea {
|
||||
min-height: 180px;
|
||||
resize: vertical;
|
||||
}
|
||||
.btn-group {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 15px;
|
||||
}
|
||||
button {
|
||||
background: linear-gradient(135deg, #4c51bf 0%, #553c9a 100%);
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
padding: 12px 24px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
button:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(76, 81, 191, 0.4);
|
||||
}
|
||||
button.secondary {
|
||||
background: #f8f9fa;
|
||||
color: #333;
|
||||
}
|
||||
button.secondary:hover {
|
||||
background: #e9ecef;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.result-box {
|
||||
background: #f8f9fa;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
margin-top: 15px;
|
||||
font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
|
||||
font-size: 12px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.result-box.error {
|
||||
background: #fff5f5;
|
||||
border-left: 4px solid #e53e3e;
|
||||
}
|
||||
.result-box.success {
|
||||
background: #f0fff4;
|
||||
border-left: 4px solid #38a169;
|
||||
}
|
||||
.patterns-list {
|
||||
max-height: 250px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.pattern-item {
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
font-size: 12px;
|
||||
}
|
||||
.pattern-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.pattern-item code {
|
||||
background: #e9ecef;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
|
||||
}
|
||||
.pattern-item .arrow {
|
||||
color: #553c9a;
|
||||
margin: 0 8px;
|
||||
}
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.tab {
|
||||
padding: 8px 16px;
|
||||
background: #f0f0f0;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
}
|
||||
.tab.active {
|
||||
background: linear-gradient(135deg, #4c51bf 0%, #553c9a 100%);
|
||||
color: #ffffff;
|
||||
}
|
||||
.highlight {
|
||||
background: #fff3cd;
|
||||
padding: 1px 4px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.stat-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.stat-box {
|
||||
text-align: center;
|
||||
padding: 15px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.stat-box .value {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #667eea;
|
||||
}
|
||||
.stat-box .label {
|
||||
font-size: 11px;
|
||||
color: #888;
|
||||
margin-top: 5px;
|
||||
}
|
||||
footer {
|
||||
text-align: center;
|
||||
color: white;
|
||||
margin-top: 30px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
footer a {
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1>GDPR Pattern Tester</h1>
|
||||
<p>Test and validate regex patterns for masking sensitive data in log messages</p>
|
||||
</header>
|
||||
|
||||
<div class="grid">
|
||||
<div class="card">
|
||||
<h2>Sample Input</h2>
|
||||
<label for="sampleText">Enter text containing sensitive data:</label>
|
||||
<textarea id="sampleText">User john.doe@example.com logged in from 192.168.1.100.
|
||||
Credit card: 4532-1234-5678-9012
|
||||
SSN: 123-45-6789
|
||||
Phone: +1 (555) 123-4567
|
||||
Finnish SSN: 131052-308T
|
||||
IBAN: FI21 1234 5600 0007 85</textarea>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Custom Patterns</h2>
|
||||
<label for="patterns">JSON patterns (pattern => replacement):</label>
|
||||
<textarea id="patterns">{
|
||||
"/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}/": "[EMAIL]",
|
||||
"/\\b\\d{3}-\\d{2}-\\d{4}\\b/": "***-**-****",
|
||||
"/\\b\\d{4}[\\s-]?\\d{4}[\\s-]?\\d{4}[\\s-]?\\d{4}\\b/": "****-****-****-****",
|
||||
"/\\b\\d{6}[-+A]\\d{3}[A-Z0-9]\\b/": "******-****"
|
||||
}</textarea>
|
||||
<div class="btn-group">
|
||||
<button class="secondary" onclick="loadDefaults()">Load Defaults</button>
|
||||
<button class="secondary" onclick="clearPatterns()">Clear</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="grid-column: 1 / -1;">
|
||||
<h2>Actions</h2>
|
||||
<div class="btn-group">
|
||||
<button onclick="testPatterns()">Test Patterns</button>
|
||||
<button onclick="testProcessor()">Test Full Processor</button>
|
||||
<button onclick="testStrategies()">Test with Strategies</button>
|
||||
<button class="secondary" onclick="validatePatterns()">Validate Patterns</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Masked Output</h2>
|
||||
<div id="maskedOutput" class="result-box">Masked output will appear here...</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Pattern Matches</h2>
|
||||
<div id="matchesOutput" class="result-box">Matches will appear here...</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Default Patterns</h2>
|
||||
<div id="defaultPatterns" class="patterns-list">Loading default patterns...</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Audit Log</h2>
|
||||
<div id="auditLog" class="result-box">Audit log entries will appear here...</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="grid-column: 1 / -1;">
|
||||
<h2>Full Results</h2>
|
||||
<div id="fullResults" class="result-box">Complete results will appear here...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<p>
|
||||
<a href="https://github.com/ivuorinen/monolog-gdpr-filter" target="_blank">
|
||||
ivuorinen/monolog-gdpr-filter
|
||||
</a>
|
||||
— Run with: <code>php -S localhost:8080 demo/index.php</code>
|
||||
</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function api(action, data = {}) {
|
||||
try {
|
||||
const response = await fetch(globalThis.location.href, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ action, ...data })
|
||||
});
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
return { error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
function getPatterns() {
|
||||
try {
|
||||
return JSON.parse(document.getElementById('patterns').value);
|
||||
} catch (error) {
|
||||
showError('Invalid JSON in patterns field: ' + error.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function testPatterns() {
|
||||
const text = document.getElementById('sampleText').value;
|
||||
const patterns = getPatterns();
|
||||
|
||||
if (!patterns) {
|
||||
showError('Invalid JSON in patterns field');
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await api('test_patterns', { text, patterns });
|
||||
|
||||
if (result.error) {
|
||||
showError(result.error);
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('maskedOutput').textContent = result.masked || '';
|
||||
document.getElementById('maskedOutput').className = 'result-box success';
|
||||
|
||||
if (result.matches && Object.keys(result.matches).length > 0) {
|
||||
document.getElementById('matchesOutput').textContent =
|
||||
JSON.stringify(result.matches, null, 2);
|
||||
} else {
|
||||
document.getElementById('matchesOutput').textContent = 'No matches found';
|
||||
}
|
||||
|
||||
document.getElementById('fullResults').textContent =
|
||||
JSON.stringify(result, null, 2);
|
||||
|
||||
if (result.errors && result.errors.length > 0) {
|
||||
document.getElementById('fullResults').className = 'result-box error';
|
||||
} else {
|
||||
document.getElementById('fullResults').className = 'result-box success';
|
||||
}
|
||||
}
|
||||
|
||||
async function testProcessor() {
|
||||
const message = document.getElementById('sampleText').value;
|
||||
const patterns = getPatterns() || {};
|
||||
|
||||
const result = await api('test_processor', { message, patterns });
|
||||
|
||||
if (result.error) {
|
||||
showError(result.error);
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('maskedOutput').textContent =
|
||||
result.masked_message || '';
|
||||
document.getElementById('maskedOutput').className = 'result-box success';
|
||||
|
||||
if (result.audit_log && result.audit_log.length > 0) {
|
||||
document.getElementById('auditLog').textContent =
|
||||
JSON.stringify(result.audit_log, null, 2);
|
||||
} else {
|
||||
document.getElementById('auditLog').textContent = 'No audit entries';
|
||||
}
|
||||
|
||||
document.getElementById('fullResults').textContent =
|
||||
JSON.stringify(result, null, 2);
|
||||
document.getElementById('fullResults').className = 'result-box success';
|
||||
}
|
||||
|
||||
async function testStrategies() {
|
||||
const message = document.getElementById('sampleText').value;
|
||||
const patterns = getPatterns() || {};
|
||||
|
||||
const result = await api('test_strategies', { message, patterns });
|
||||
|
||||
document.getElementById('maskedOutput').textContent =
|
||||
result.masked_message || '';
|
||||
document.getElementById('maskedOutput').className = 'result-box success';
|
||||
|
||||
if (result.strategy_stats) {
|
||||
document.getElementById('matchesOutput').textContent =
|
||||
'Strategy Statistics:\n' + JSON.stringify(result.strategy_stats, null, 2);
|
||||
}
|
||||
|
||||
document.getElementById('fullResults').textContent =
|
||||
JSON.stringify(result, null, 2);
|
||||
document.getElementById('fullResults').className = 'result-box success';
|
||||
}
|
||||
|
||||
async function validatePatterns() {
|
||||
const patterns = getPatterns();
|
||||
|
||||
if (!patterns) {
|
||||
showError('Invalid JSON in patterns field');
|
||||
return;
|
||||
}
|
||||
|
||||
const results = [];
|
||||
for (const pattern of Object.keys(patterns)) {
|
||||
const result = await api('validate_pattern', { pattern });
|
||||
results.push({
|
||||
pattern,
|
||||
valid: result.valid,
|
||||
error: result.error
|
||||
});
|
||||
}
|
||||
|
||||
const valid = results.filter(r => r.valid).length;
|
||||
const invalid = results.filter(r => !r.valid).length;
|
||||
|
||||
document.getElementById('fullResults').textContent =
|
||||
`Validation Results: ${valid} valid, ${invalid} invalid\n\n` +
|
||||
JSON.stringify(results, null, 2);
|
||||
|
||||
document.getElementById('fullResults').className =
|
||||
invalid > 0 ? 'result-box error' : 'result-box success';
|
||||
}
|
||||
|
||||
async function loadDefaults() {
|
||||
const result = await api('get_defaults');
|
||||
if (result.patterns) {
|
||||
document.getElementById('patterns').value =
|
||||
JSON.stringify(result.patterns, null, 4);
|
||||
}
|
||||
}
|
||||
|
||||
function clearPatterns() {
|
||||
document.getElementById('patterns').value = '{\n \n}';
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
document.getElementById('fullResults').textContent = 'Error: ' + message;
|
||||
document.getElementById('fullResults').className = 'result-box error';
|
||||
}
|
||||
|
||||
// Load default patterns on page load
|
||||
async function loadDefaultPatternsOnInit() {
|
||||
try {
|
||||
const result = await api('get_defaults');
|
||||
if (result.patterns) {
|
||||
const container = document.getElementById('defaultPatterns');
|
||||
container.innerHTML = Object.entries(result.patterns)
|
||||
.map(([pattern, replacement]) =>
|
||||
`<div class="pattern-item">
|
||||
<code>${escapeHtml(pattern)}</code>
|
||||
<span class="arrow">→</span>
|
||||
<code>${escapeHtml(replacement)}</code>
|
||||
</div>`
|
||||
).join('');
|
||||
}
|
||||
} catch (error) {
|
||||
const container = document.getElementById('defaultPatterns');
|
||||
container.textContent = 'Error loading default patterns: ' + error.message;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize on page load
|
||||
loadDefaultPatternsOnInit().catch(error => {
|
||||
console.error('Failed to initialize:', error);
|
||||
});
|
||||
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user