- Add comprehensive health check system with multiple endpoints - Add Prometheus metrics endpoint - Add production logging configurations (5 strategies) - Add complete deployment documentation suite: * QUICKSTART.md - 30-minute deployment guide * DEPLOYMENT_CHECKLIST.md - Printable verification checklist * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference * production-logging.md - Logging configuration guide * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation * README.md - Navigation hub * DEPLOYMENT_SUMMARY.md - Executive summary - Add deployment scripts and automation - Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment - Update README with production-ready features All production infrastructure is now complete and ready for deployment.
288 lines
10 KiB
PHP
288 lines
10 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Framework\Logging\Security\SensitiveDataRedactor;
|
|
use App\Framework\Logging\Security\RedactionMode;
|
|
|
|
describe('SensitiveDataRedactor', function () {
|
|
describe('constructor', function () {
|
|
it('accepts default configuration', function () {
|
|
$redactor = new SensitiveDataRedactor();
|
|
expect($redactor)->toBeInstanceOf(SensitiveDataRedactor::class);
|
|
});
|
|
|
|
it('accepts custom RedactionMode', function () {
|
|
$redactor = new SensitiveDataRedactor(RedactionMode::FULL);
|
|
expect($redactor)->toBeInstanceOf(SensitiveDataRedactor::class);
|
|
});
|
|
|
|
it('accepts email and IP redaction flags', function () {
|
|
$redactor = new SensitiveDataRedactor(
|
|
redactEmails: true,
|
|
redactIps: true
|
|
);
|
|
expect($redactor)->toBeInstanceOf(SensitiveDataRedactor::class);
|
|
});
|
|
});
|
|
|
|
describe('key-based redaction', function () {
|
|
it('redacts password fields', function () {
|
|
$redactor = new SensitiveDataRedactor(RedactionMode::FULL);
|
|
$data = ['username' => 'john', 'password' => 'secret123'];
|
|
|
|
$redacted = $redactor->redact($data);
|
|
|
|
expect($redacted['username'])->toBe('john');
|
|
expect($redacted['password'])->toBe('[REDACTED]');
|
|
});
|
|
|
|
it('redacts API key fields', function () {
|
|
$redactor = new SensitiveDataRedactor(RedactionMode::FULL);
|
|
$data = ['api_key' => 'sk_live_1234567890abcdef'];
|
|
|
|
$redacted = $redactor->redact($data);
|
|
|
|
expect($redacted['api_key'])->toBe('[REDACTED]');
|
|
});
|
|
|
|
it('redacts multiple sensitive fields', function () {
|
|
$redactor = new SensitiveDataRedactor(RedactionMode::FULL);
|
|
$data = [
|
|
'username' => 'john',
|
|
'password' => 'secret',
|
|
'api_key' => 'key123',
|
|
'token' => 'token456',
|
|
'user_id' => 42
|
|
];
|
|
|
|
$redacted = $redactor->redact($data);
|
|
|
|
expect($redacted['username'])->toBe('john');
|
|
expect($redacted['password'])->toBe('[REDACTED]');
|
|
expect($redacted['api_key'])->toBe('[REDACTED]');
|
|
expect($redacted['token'])->toBe('[REDACTED]');
|
|
expect($redacted['user_id'])->toBe(42);
|
|
});
|
|
|
|
it('handles nested arrays', function () {
|
|
$redactor = new SensitiveDataRedactor(RedactionMode::FULL);
|
|
$data = [
|
|
'user' => [
|
|
'name' => 'John Doe',
|
|
'password' => 'secret',
|
|
'preferences' => [
|
|
'theme' => 'dark',
|
|
'api_key' => 'key123'
|
|
]
|
|
]
|
|
];
|
|
|
|
$redacted = $redactor->redact($data);
|
|
|
|
expect($redacted['user']['name'])->toBe('John Doe');
|
|
expect($redacted['user']['password'])->toBe('[REDACTED]');
|
|
expect($redacted['user']['preferences']['theme'])->toBe('dark');
|
|
expect($redacted['user']['preferences']['api_key'])->toBe('[REDACTED]');
|
|
});
|
|
});
|
|
|
|
describe('redaction modes', function () {
|
|
it('uses FULL mode to completely mask values', function () {
|
|
$redactor = new SensitiveDataRedactor(RedactionMode::FULL);
|
|
$data = ['password' => 'super-secret-password'];
|
|
|
|
$redacted = $redactor->redact($data);
|
|
|
|
expect($redacted['password'])->toBe('[REDACTED]');
|
|
});
|
|
|
|
it('uses PARTIAL mode to partially mask values', function () {
|
|
$redactor = new SensitiveDataRedactor(RedactionMode::PARTIAL);
|
|
$data = ['password' => 'super-secret-password'];
|
|
|
|
$redacted = $redactor->redact($data);
|
|
|
|
expect($redacted['password'])->toStartWith('su');
|
|
expect($redacted['password'])->toEndWith('rd');
|
|
expect($redacted['password'])->toContain('*');
|
|
});
|
|
|
|
it('uses HASH mode for deterministic redaction', function () {
|
|
$redactor = new SensitiveDataRedactor(RedactionMode::HASH);
|
|
$data = ['password' => 'test123'];
|
|
|
|
$redacted = $redactor->redact($data);
|
|
|
|
expect($redacted['password'])->toStartWith('[HASH:');
|
|
expect($redacted['password'])->toEndWith(']');
|
|
|
|
// Same input should produce same hash
|
|
$redacted2 = $redactor->redact($data);
|
|
expect($redacted['password'])->toBe($redacted2['password']);
|
|
});
|
|
});
|
|
|
|
describe('content-based redaction', function () {
|
|
it('redacts credit card numbers', function () {
|
|
$redactor = new SensitiveDataRedactor();
|
|
$message = 'Payment with card 4532-1234-5678-9010';
|
|
|
|
$redacted = $redactor->redactString($message);
|
|
|
|
expect($redacted)->toContain('[CREDIT_CARD]');
|
|
expect(str_contains($redacted, '4532-1234-5678-9010'))->toBeFalsy();
|
|
});
|
|
|
|
it('redacts Bearer tokens', function () {
|
|
$redactor = new SensitiveDataRedactor();
|
|
$message = 'Auth: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.payload.signature';
|
|
|
|
$redacted = $redactor->redactString($message);
|
|
|
|
expect($redacted)->toContain('Bearer [REDACTED]');
|
|
expect(str_contains($redacted, 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9'))->toBeFalsy();
|
|
});
|
|
|
|
it('redacts SSN numbers', function () {
|
|
$redactor = new SensitiveDataRedactor();
|
|
$message = 'SSN: 123-45-6789';
|
|
|
|
$redacted = $redactor->redactString($message);
|
|
|
|
expect($redacted)->toContain('[SSN]');
|
|
expect(str_contains($redacted, '123-45-6789'))->toBeFalsy();
|
|
});
|
|
|
|
it('redacts email addresses when enabled', function () {
|
|
$redactor = new SensitiveDataRedactor(redactEmails: true);
|
|
$message = 'Contact: john.doe@example.com';
|
|
|
|
$redacted = $redactor->redactString($message);
|
|
|
|
expect($redacted)->toContain('@example.com');
|
|
expect(str_contains($redacted, 'john.doe'))->toBeFalsy();
|
|
});
|
|
|
|
it('does not redact email addresses when disabled', function () {
|
|
$redactor = new SensitiveDataRedactor(redactEmails: false);
|
|
$message = 'Contact: john.doe@example.com';
|
|
|
|
$redacted = $redactor->redactString($message);
|
|
|
|
expect($redacted)->toContain('john.doe@example.com');
|
|
});
|
|
|
|
it('redacts IP addresses when enabled', function () {
|
|
$redactor = new SensitiveDataRedactor(redactIps: true);
|
|
$message = 'Request from 192.168.1.100';
|
|
|
|
$redacted = $redactor->redactString($message);
|
|
|
|
expect($redacted)->toContain('[IP_ADDRESS]');
|
|
expect(str_contains($redacted, '192.168.1.100'))->toBeFalsy();
|
|
});
|
|
|
|
it('does not redact IP addresses when disabled', function () {
|
|
$redactor = new SensitiveDataRedactor(redactIps: false);
|
|
$message = 'Request from 192.168.1.100';
|
|
|
|
$redacted = $redactor->redactString($message);
|
|
|
|
expect($redacted)->toContain('192.168.1.100');
|
|
});
|
|
});
|
|
|
|
describe('factory methods', function () {
|
|
it('creates production redactor with full redaction', function () {
|
|
$redactor = SensitiveDataRedactor::production();
|
|
$data = ['password' => 'secret', 'user' => 'john'];
|
|
|
|
$redacted = $redactor->redact($data);
|
|
|
|
expect($redacted['password'])->toBe('[REDACTED]');
|
|
expect($redacted['user'])->toBe('john');
|
|
});
|
|
|
|
it('creates development redactor with partial redaction', function () {
|
|
$redactor = SensitiveDataRedactor::development();
|
|
$data = ['password' => 'super-secret-password'];
|
|
|
|
$redacted = $redactor->redact($data);
|
|
|
|
expect($redacted['password'])->toStartWith('su');
|
|
expect($redacted['password'])->toEndWith('rd');
|
|
});
|
|
|
|
it('creates testing redactor with hash-based redaction', function () {
|
|
$redactor = SensitiveDataRedactor::testing();
|
|
$data = ['password' => 'test123'];
|
|
|
|
$redacted = $redactor->redact($data);
|
|
|
|
expect($redacted['password'])->toStartWith('[HASH:');
|
|
});
|
|
});
|
|
|
|
describe('edge cases', function () {
|
|
it('handles empty arrays', function () {
|
|
$redactor = new SensitiveDataRedactor();
|
|
$redacted = $redactor->redact([]);
|
|
|
|
expect($redacted)->toBe([]);
|
|
});
|
|
|
|
it('handles empty strings', function () {
|
|
$redactor = new SensitiveDataRedactor();
|
|
$redacted = $redactor->redactString('');
|
|
|
|
expect($redacted)->toBe('');
|
|
});
|
|
|
|
it('handles arrays with array values in sensitive fields', function () {
|
|
$redactor = new SensitiveDataRedactor(RedactionMode::FULL);
|
|
$data = ['password' => ['old' => 'secret1', 'new' => 'secret2']];
|
|
|
|
$redacted = $redactor->redact($data);
|
|
|
|
expect($redacted['password'])->toBeArray();
|
|
expect($redacted['password']['old'])->toBe('[REDACTED]');
|
|
expect($redacted['password']['new'])->toBe('[REDACTED]');
|
|
});
|
|
|
|
it('handles non-string values in sensitive fields', function () {
|
|
$redactor = new SensitiveDataRedactor(RedactionMode::FULL);
|
|
$data = ['password' => 12345];
|
|
|
|
$redacted = $redactor->redact($data);
|
|
|
|
expect($redacted['password'])->toBe('[REDACTED]');
|
|
});
|
|
|
|
it('handles short passwords in partial mode', function () {
|
|
$redactor = new SensitiveDataRedactor(RedactionMode::PARTIAL);
|
|
$data = ['password' => 'ab'];
|
|
|
|
$redacted = $redactor->redact($data);
|
|
|
|
expect($redacted['password'])->toBe('**');
|
|
});
|
|
|
|
it('handles unicode characters', function () {
|
|
$redactor = new SensitiveDataRedactor(RedactionMode::PARTIAL);
|
|
$data = ['password' => 'Passwörd123'];
|
|
|
|
$redacted = $redactor->redact($data);
|
|
|
|
expect($redacted['password'])->toContain('*');
|
|
});
|
|
});
|
|
|
|
describe('readonly behavior', function () {
|
|
it('is a readonly class', function () {
|
|
$reflection = new ReflectionClass(SensitiveDataRedactor::class);
|
|
expect($reflection->isReadOnly())->toBeTrue();
|
|
});
|
|
});
|
|
});
|