diff --git a/CHANGELOG.md b/CHANGELOG.md index ada3d85..96ecbd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,16 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://git.d3data.de/D3Public/sensitiveMessageFormatter/compare/1.1.0...rel_1.x) -## [1.1.0](https://git.d3data.de/D3Public/sensitiveMessageFormatter/compare/1.0.1...1.1.0) - 2025-01-07 -### Added -- optional fixed length replacement - -## [1.0.1](https://git.d3data.de/D3Public/sensitiveMessageFormatter/compare/1.0.0...1.0.1) - 2025-01-01 -### Changed -- Guzzle dependency from 7.0 - ## [1.0.0](https://git.d3data.de/D3Public/sensitiveMessageFormatter/releases/tag/1.0.0) - 2024-12-23 ### Added - initial implementation - - replace defined fixed strings by replacement characters - - search for urlencoded strings too + - replace regex + - replace plain string (+ urlencoded if required) + - optional replacement string configuration diff --git a/README.md b/README.md index 3e302c1..98c6ca3 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ $stack->push( $myLogger, new sensitiveMessageFormatter( '{method} {uri} {req_body} - RESPONSE: {code} - {res_body}', - ['myUsername', 'mySecretPassword'] + ['myUsername', '/my.*Password/i'] ) ), Logger::INFO diff --git a/src/sensitiveMessageFormatter.php b/src/sensitiveMessageFormatter.php index 2011ce7..4713e57 100644 --- a/src/sensitiveMessageFormatter.php +++ b/src/sensitiveMessageFormatter.php @@ -27,15 +27,15 @@ class sensitiveMessageFormatter extends MessageFormatter /** * @param string|null $template * @param string[] $anonymizations - * @param string|null $replaceChar + * @param string|null $replacement */ public function __construct( ?string $template = self::CLF, protected array $anonymizations = [], - protected ?string $replaceChar = null, - protected ?int $fixedReplacementLength = null + protected ?string $replacement = null ) { - $this->createReplacements($this->anonymizations); + $this->replacement ??= '*****'; + $this->convertStringsToRegex($this->anonymizations); parent::__construct($template); } @@ -44,27 +44,23 @@ class sensitiveMessageFormatter extends MessageFormatter * @param string[] $search * @return void */ - protected function createReplacements(array $search = []): void + protected function convertStringsToRegex(array $search = []): void { - $replacements = []; - - $this->replaceChar ??= '*'; - array_map( - function ($search) use (&$replacements) { - $replacements[$search] = str_repeat( - $this->replaceChar, - $this->fixedReplacementLength ?? strlen($search) - ); - $replacements[urlencode($search)] = str_repeat( - $this->replaceChar, - $this->fixedReplacementLength ?? strlen($search) - ); + function ($search) use (&$searchStrings) { + if (!$this->stringIsRegexp($search)) { + $searchStrings[] = '/'.preg_quote($search, '/').'/i'; + if (urlencode($search) !== $search) { + $searchStrings[] = '/' . preg_quote(urlencode($search), '/') . '/i'; + } + } else { + $searchStrings[] = $search; + } }, $search ); - $this->anonymizations = $replacements; + $this->anonymizations = $searchStrings; } public function format( @@ -75,13 +71,18 @@ class sensitiveMessageFormatter extends MessageFormatter $result = parent::format($request, $response, $error); if (count($this->anonymizations)) { - $result = str_replace( - array_keys($this->anonymizations), - array_values($this->anonymizations), + $result = preg_replace( + $this->anonymizations, + $this->replacement, $result ); } return $result; } + + protected function stringIsRegexp(string $string): bool + { + return @preg_match($string, '') !== false; + } } diff --git a/tests/sensitiveMessageFormatterTest.php b/tests/sensitiveMessageFormatterTest.php index 74991c1..adf511d 100644 --- a/tests/sensitiveMessageFormatterTest.php +++ b/tests/sensitiveMessageFormatterTest.php @@ -19,6 +19,7 @@ namespace D3\SensitiveMessageFormatter\tests; use D3\SensitiveMessageFormatter\sensitiveMessageFormatter; use Generator; +use GuzzleHttp\MessageFormatter; use GuzzleHttp\Psr7\Request; use GuzzleHttp\Psr7\Response; use ReflectionException; @@ -28,90 +29,74 @@ use ReflectionException; */ class sensitiveMessageFormatterTest extends ApiTestCase { - protected sensitiveMessageFormatter $sut; - - public function setUp(): void - { - parent::setUp(); - - $this->sut = new sensitiveMessageFormatter( - '{method} {uri} HTTP/{version} {req_headers} {req_body} -- RESPONSE: {code} - {res_headers} {res_body}' - ); - } - /** * @test * @throws ReflectionException * @covers \D3\SensitiveMessageFormatter\sensitiveMessageFormatter::__construct + * @dataProvider constructDataProvider */ - public function testConstruct(): void + public function testConstruct(?string $replacement, string $expected): void { $sutMock = $this->getMockBuilder(sensitiveMessageFormatter::class) ->disableOriginalConstructor() - ->onlyMethods(['createReplacements']) + ->onlyMethods(['convertStringsToRegex']) ->getMock(); - $sutMock->expects($this->once())->method('createReplacements'); + $sutMock->expects($this->once())->method('convertStringsToRegex'); - $sutMock->__construct(); + $sutMock->__construct(MessageFormatter::CLF, [], $replacement); $this->assertSame( - '*', + $expected, $this->getValue( - $this->sut, - 'replaceChar' + $sutMock, + 'replacement' ) ); } + public static function constructDataProvider(): Generator + { + yield 'without custom replacement' => [null, '*****']; + yield 'with custom replacement' => ['-.-', '-.-']; + } + /** * @test * @throws ReflectionException - * @dataProvider createReplacementsDataProvider - * @covers \D3\SensitiveMessageFormatter\sensitiveMessageFormatter::createReplacements + * @dataProvider convertStringsToRegexDataProvider + * @covers \D3\SensitiveMessageFormatter\sensitiveMessageFormatter::convertStringsToRegex */ - public function testCreateReplacements( + public function testConvertStringsToRegex( array $input, - array $expected, - ?string $replacement = null, - ?int $fixedReplacementLenth = null - ): void - { - if ($replacement) { - $this->setValue( - $this->sut, - 'replaceChar', - $replacement - ); - } - - $this->setValue( - $this->sut, - 'fixedReplacementLength', - $fixedReplacementLenth - ); + array $expected + ): void { + $sutMock = $this->getMockBuilder(sensitiveMessageFormatter::class) + ->disableOriginalConstructor() + ->getMock(); $this->callMethod( - $this->sut, - 'createReplacements', + $sutMock, + 'convertStringsToRegex', [$input] ); $this->assertSame( $expected, $this->getValue( - $this->sut, + $sutMock, 'anonymizations' ) ); } - public static function createReplacementsDataProvider(): Generator + public static function convertStringsToRegexDataProvider(): Generator { - yield 'simple' => [['abc'], ['abc' => '***']]; - yield 'multiple' => [['def', 'def'], ['def' => '***']]; - yield 'urlencoded' => [['1&c'], ['1&c' => '***', '1%26c' => '***' ]]; - yield 'different replace char' => [['abcd'], ['abcd' => '####'], '#']; - yield 'fixed replacement lenght' => [['abcd'], ['abcd' => '*******'], '*', 7]; + yield 'simple' => [['abc'], ['/abc/i']]; + yield 'multiple' => [['def', 'ghi'], ['/def/i', '/ghi/i']]; + yield 'urlencoded' => [['1&c'], ['/1&c/i', '/1%26c/i']]; + yield 'delimiter' => [['de/fg'], ['/de\/fg/i', '/de%2Ffg/i']]; + yield 'regex' => [['/abc/mi'], ['/abc/mi']]; + yield 'mixed' => [['/abc/mi', 'def', '/ghi/mi'], ['/abc/mi', '/def/i', '/ghi/mi']]; } /** @@ -122,17 +107,16 @@ class sensitiveMessageFormatterTest extends ApiTestCase */ public function testFormat(array $replacements, $expected): void { - $this->callMethod( - $this->sut, - 'createReplacements', - [$replacements] + $sut = new sensitiveMessageFormatter( + '{method} {uri} HTTP/{version} {req_headers} {req_body} -- RESPONSE: {code} - {res_headers} {res_body}', + $replacements ); $request = new Request( 'POST', 'google.com', ['header1' => 'value1', 'header2' => 'val%26ue2'], - 'Body value1 + value2' + 'Body value1 + value2, Body value1 + value2 aBodyb' ); $response = new Response( 200, @@ -146,7 +130,7 @@ class sensitiveMessageFormatterTest extends ApiTestCase '@(\r\n|\r|\n)@', '==', $this->callMethod( - $this->sut, + $sut, 'format', [$request, $response] ) @@ -156,11 +140,23 @@ class sensitiveMessageFormatterTest extends ApiTestCase public static function formatDataProvider(): Generator { - yield [ + yield 'plain' => [ ['value1', 'val&ue2'], 'POST google.com HTTP/1.1 POST google.com HTTP/1.1=='. - 'header1: ******==header2: ******* Body ****** + value2 -- RESPONSE: 200 - HTTP/1.1 200 OK=='. - 'header1: ******==header2: value2 Body ****** + value2', + 'header1: *****==header2: ***** Body ***** + value2, Body ***** + value2 aBodyb -- RESPONSE: 200 - HTTP/1.1 200 OK=='. + 'header1: *****==header2: value2 Body ***** + value2', + ]; + yield 'regex placeholders' => [ + ['/go.?gle/i'], + 'POST *****.com HTTP/1.1 POST *****.com HTTP/1.1=='. + 'header1: value1==header2: val%26ue2 Body value1 + value2, Body value1 + value2 aBodyb -- RESPONSE: 200 - HTTP/1.1 200 OK=='. + 'header1: value1==header2: value2 Body value1 + value2', + ]; + yield 'regex control chars + look behind' => [ + ['/\bbody\b/i', '/(?<=RESPONSE:\s)200/i'], + 'POST google.com HTTP/1.1 POST google.com HTTP/1.1=='. + 'header1: value1==header2: val%26ue2 ***** value1 + value2, ***** value1 + value2 aBodyb -- RESPONSE: ***** - HTTP/1.1 200 OK=='. + 'header1: value1==header2: value2 ***** value1 + value2', ]; } }