record('outbound api call', [ 'authorization' => 'Bearer irdb_svc_ABCDEFGHIJKLMNOPQRSTUVWXYZ234567', ]); $out = $processor($record); // Key-based redaction wins; the whole value is replaced with ***. self::assertSame('***', $out->context['authorization']); } public function testBareTokenInMessageIsScrubbed(): void { $processor = new SecretScrubbingProcessor(); $record = $this->record( 'attempted with token irdb_adm_ABCDEFGHIJKLMNOPQRSTUVWXYZ234567', [] ); $out = $processor($record); self::assertStringContainsString('irdb_adm_***', $out->message); self::assertStringNotContainsString('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567', $out->message); } public function testFormattedOutputDoesNotLeakBearerToken(): void { // Round-trip the record through the processor and the JsonFormatter // to confirm the *rendered* log line is clean — what actually hits // stdout in production. $processor = new SecretScrubbingProcessor(); $record = $this->record('api request', [ 'headers' => ['Authorization' => 'Bearer irdb_rep_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'], ]); $out = $processor($record); $formatter = new JsonFormatter(); $line = $formatter->format($out); self::assertStringNotContainsString('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', $line); self::assertStringContainsString('***', $line); } public function testPasswordHashKeyIsScrubbed(): void { $processor = new SecretScrubbingProcessor(); $record = $this->record('config dump', [ 'LOCAL_ADMIN_PASSWORD_HASH' => '$argon2id$v=19$m=65536,t=4,p=1$abcdef$ghijkl', 'OIDC_CLIENT_SECRET' => 'super-secret-value', 'MAXMIND_LICENSE_KEY' => 'license-12345', 'IPINFO_TOKEN' => 'token-67890', 'DB_MYSQL_PASSWORD' => 'rootpass', ]); $out = $processor($record); foreach (['LOCAL_ADMIN_PASSWORD_HASH', 'OIDC_CLIENT_SECRET', 'MAXMIND_LICENSE_KEY', 'IPINFO_TOKEN', 'DB_MYSQL_PASSWORD'] as $key) { self::assertSame('***', $out->context[$key], "key {$key} not scrubbed"); } } public function testArgon2HashEmbeddedInMessageIsScrubbed(): void { $processor = new SecretScrubbingProcessor(); $record = $this->record( 'verifying $argon2id$v=19$m=65536,t=4,p=1$abc$def for user', [] ); $out = $processor($record); self::assertStringNotContainsString('$argon2id$v=19', $out->message); self::assertStringContainsString('$argon2***', $out->message); } public function testNonSensitiveContentIsLeftAlone(): void { $processor = new SecretScrubbingProcessor(); $record = $this->record('user search hit', [ 'count' => 42, 'email' => 'someone@example.com', 'ip' => '203.0.113.42', ]); $out = $processor($record); self::assertSame(42, $out->context['count']); self::assertSame('someone@example.com', $out->context['email']); self::assertSame('203.0.113.42', $out->context['ip']); self::assertSame('user search hit', $out->message); } public function testNestedContextIsScrubbedRecursively(): void { $processor = new SecretScrubbingProcessor(); $record = $this->record('nested', [ 'request' => [ 'method' => 'POST', 'authorization' => 'Bearer irdb_adm_ABCDEFGHIJKLMNOPQRSTUVWXYZ234567', 'body' => ['ok' => true], ], ]); $out = $processor($record); self::assertSame('***', $out->context['request']['authorization']); self::assertSame('POST', $out->context['request']['method']); self::assertTrue($out->context['request']['body']['ok']); } /** * @param array $context */ private function record(string $message, array $context): LogRecord { return new LogRecord( datetime: new \DateTimeImmutable(), channel: 'test', level: Level::Info, message: $message, context: $context, ); } }