refactor to use regex as well as plain strings, removed variable replacement lengths

This commit is contained in:
Daniel Seifert 2025-01-09 23:33:01 +01:00
parent 40f439f1c4
commit 4fcef7d244
Signed by: DanielS
GPG Key ID: 6A513E13AEE66170
4 changed files with 80 additions and 90 deletions

View File

@ -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) ## [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 ## [1.0.0](https://git.d3data.de/D3Public/sensitiveMessageFormatter/releases/tag/1.0.0) - 2024-12-23
### Added ### Added
- initial implementation - initial implementation
- replace defined fixed strings by replacement characters - replace regex
- search for urlencoded strings too - replace plain string (+ urlencoded if required)
- optional replacement string configuration

View File

@ -25,7 +25,7 @@ $stack->push(
$myLogger, $myLogger,
new sensitiveMessageFormatter( new sensitiveMessageFormatter(
'{method} {uri} {req_body} - RESPONSE: {code} - {res_body}', '{method} {uri} {req_body} - RESPONSE: {code} - {res_body}',
['myUsername', 'mySecretPassword'] ['myUsername', '/my.*Password/i']
) )
), ),
Logger::INFO Logger::INFO

View File

@ -27,15 +27,15 @@ class sensitiveMessageFormatter extends MessageFormatter
/** /**
* @param string|null $template * @param string|null $template
* @param string[] $anonymizations * @param string[] $anonymizations
* @param string|null $replaceChar * @param string|null $replacement
*/ */
public function __construct( public function __construct(
?string $template = self::CLF, ?string $template = self::CLF,
protected array $anonymizations = [], protected array $anonymizations = [],
protected ?string $replaceChar = null, protected ?string $replacement = null
protected ?int $fixedReplacementLength = null
) { ) {
$this->createReplacements($this->anonymizations); $this->replacement ??= '*****';
$this->convertStringsToRegex($this->anonymizations);
parent::__construct($template); parent::__construct($template);
} }
@ -44,27 +44,23 @@ class sensitiveMessageFormatter extends MessageFormatter
* @param string[] $search * @param string[] $search
* @return void * @return void
*/ */
protected function createReplacements(array $search = []): void protected function convertStringsToRegex(array $search = []): void
{ {
$replacements = [];
$this->replaceChar ??= '*';
array_map( array_map(
function ($search) use (&$replacements) { function ($search) use (&$searchStrings) {
$replacements[$search] = str_repeat( if (!$this->stringIsRegexp($search)) {
$this->replaceChar, $searchStrings[] = '/'.preg_quote($search, '/').'/i';
$this->fixedReplacementLength ?? strlen($search) if (urlencode($search) !== $search) {
); $searchStrings[] = '/' . preg_quote(urlencode($search), '/') . '/i';
$replacements[urlencode($search)] = str_repeat( }
$this->replaceChar, } else {
$this->fixedReplacementLength ?? strlen($search) $searchStrings[] = $search;
); }
}, },
$search $search
); );
$this->anonymizations = $replacements; $this->anonymizations = $searchStrings;
} }
public function format( public function format(
@ -75,13 +71,18 @@ class sensitiveMessageFormatter extends MessageFormatter
$result = parent::format($request, $response, $error); $result = parent::format($request, $response, $error);
if (count($this->anonymizations)) { if (count($this->anonymizations)) {
$result = str_replace( $result = preg_replace(
array_keys($this->anonymizations), $this->anonymizations,
array_values($this->anonymizations), $this->replacement,
$result $result
); );
} }
return $result; return $result;
} }
protected function stringIsRegexp(string $string): bool
{
return @preg_match($string, '') !== false;
}
} }

View File

@ -19,6 +19,7 @@ namespace D3\SensitiveMessageFormatter\tests;
use D3\SensitiveMessageFormatter\sensitiveMessageFormatter; use D3\SensitiveMessageFormatter\sensitiveMessageFormatter;
use Generator; use Generator;
use GuzzleHttp\MessageFormatter;
use GuzzleHttp\Psr7\Request; use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response; use GuzzleHttp\Psr7\Response;
use ReflectionException; use ReflectionException;
@ -28,90 +29,74 @@ use ReflectionException;
*/ */
class sensitiveMessageFormatterTest extends ApiTestCase 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 * @test
* @throws ReflectionException * @throws ReflectionException
* @covers \D3\SensitiveMessageFormatter\sensitiveMessageFormatter::__construct * @covers \D3\SensitiveMessageFormatter\sensitiveMessageFormatter::__construct
* @dataProvider constructDataProvider
*/ */
public function testConstruct(): void public function testConstruct(?string $replacement, string $expected): void
{ {
$sutMock = $this->getMockBuilder(sensitiveMessageFormatter::class) $sutMock = $this->getMockBuilder(sensitiveMessageFormatter::class)
->disableOriginalConstructor() ->disableOriginalConstructor()
->onlyMethods(['createReplacements']) ->onlyMethods(['convertStringsToRegex'])
->getMock(); ->getMock();
$sutMock->expects($this->once())->method('createReplacements'); $sutMock->expects($this->once())->method('convertStringsToRegex');
$sutMock->__construct(); $sutMock->__construct(MessageFormatter::CLF, [], $replacement);
$this->assertSame( $this->assertSame(
'*', $expected,
$this->getValue( $this->getValue(
$this->sut, $sutMock,
'replaceChar' 'replacement'
) )
); );
} }
public static function constructDataProvider(): Generator
{
yield 'without custom replacement' => [null, '*****'];
yield 'with custom replacement' => ['-.-', '-.-'];
}
/** /**
* @test * @test
* @throws ReflectionException * @throws ReflectionException
* @dataProvider createReplacementsDataProvider * @dataProvider convertStringsToRegexDataProvider
* @covers \D3\SensitiveMessageFormatter\sensitiveMessageFormatter::createReplacements * @covers \D3\SensitiveMessageFormatter\sensitiveMessageFormatter::convertStringsToRegex
*/ */
public function testCreateReplacements( public function testConvertStringsToRegex(
array $input, array $input,
array $expected, array $expected
?string $replacement = null, ): void {
?int $fixedReplacementLenth = null $sutMock = $this->getMockBuilder(sensitiveMessageFormatter::class)
): void ->disableOriginalConstructor()
{ ->getMock();
if ($replacement) {
$this->setValue(
$this->sut,
'replaceChar',
$replacement
);
}
$this->setValue(
$this->sut,
'fixedReplacementLength',
$fixedReplacementLenth
);
$this->callMethod( $this->callMethod(
$this->sut, $sutMock,
'createReplacements', 'convertStringsToRegex',
[$input] [$input]
); );
$this->assertSame( $this->assertSame(
$expected, $expected,
$this->getValue( $this->getValue(
$this->sut, $sutMock,
'anonymizations' 'anonymizations'
) )
); );
} }
public static function createReplacementsDataProvider(): Generator public static function convertStringsToRegexDataProvider(): Generator
{ {
yield 'simple' => [['abc'], ['abc' => '***']]; yield 'simple' => [['abc'], ['/abc/i']];
yield 'multiple' => [['def', 'def'], ['def' => '***']]; yield 'multiple' => [['def', 'ghi'], ['/def/i', '/ghi/i']];
yield 'urlencoded' => [['1&c'], ['1&c' => '***', '1%26c' => '***' ]]; yield 'urlencoded' => [['1&c'], ['/1&c/i', '/1%26c/i']];
yield 'different replace char' => [['abcd'], ['abcd' => '####'], '#']; yield 'delimiter' => [['de/fg'], ['/de\/fg/i', '/de%2Ffg/i']];
yield 'fixed replacement lenght' => [['abcd'], ['abcd' => '*******'], '*', 7]; 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 public function testFormat(array $replacements, $expected): void
{ {
$this->callMethod( $sut = new sensitiveMessageFormatter(
$this->sut, '{method} {uri} HTTP/{version} {req_headers} {req_body} -- RESPONSE: {code} - {res_headers} {res_body}',
'createReplacements', $replacements
[$replacements]
); );
$request = new Request( $request = new Request(
'POST', 'POST',
'google.com', 'google.com',
['header1' => 'value1', 'header2' => 'val%26ue2'], ['header1' => 'value1', 'header2' => 'val%26ue2'],
'Body value1 + value2' 'Body value1 + value2, Body value1 + value2 aBodyb'
); );
$response = new Response( $response = new Response(
200, 200,
@ -146,7 +130,7 @@ class sensitiveMessageFormatterTest extends ApiTestCase
'@(\r\n|\r|\n)@', '@(\r\n|\r|\n)@',
'==', '==',
$this->callMethod( $this->callMethod(
$this->sut, $sut,
'format', 'format',
[$request, $response] [$request, $response]
) )
@ -156,11 +140,23 @@ class sensitiveMessageFormatterTest extends ApiTestCase
public static function formatDataProvider(): Generator public static function formatDataProvider(): Generator
{ {
yield [ yield 'plain' => [
['value1', 'val&ue2'], ['value1', 'val&ue2'],
'POST google.com HTTP/1.1 POST google.com HTTP/1.1=='. '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: ***** Body ***** + value2, Body ***** + value2 aBodyb -- RESPONSE: 200 - HTTP/1.1 200 OK=='.
'header1: ******==header2: value2 Body ****** + value2', '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',
]; ];
} }
} }