-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1 from stefnadev/feature/pii-anonymizer
Add support for handling anonyimze some pii data
- Loading branch information
Showing
11 changed files
with
617 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
<?php declare(strict_types=1); | ||
|
||
namespace Stefna\Logger\PII\Anonymizer; | ||
|
||
interface Anonymizer | ||
{ | ||
public function support(string $key): bool; | ||
|
||
/** | ||
* @param mixed $value | ||
* @return mixed | ||
*/ | ||
public function process(string $key, $value); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
<?php declare(strict_types=1); | ||
|
||
namespace Stefna\Logger\PII\Anonymizer; | ||
|
||
use Stefna\Logger\PII\Exception\NotSupportedType; | ||
|
||
final class CardAnonymizer implements Anonymizer | ||
{ | ||
public const CARD_CCV = '_card_ccv'; | ||
public const CARD_HOLDER = '_card_holder'; | ||
public const CARD_NUMBER = '_card_number'; | ||
|
||
public function support(string $key): bool | ||
{ | ||
return in_array($key, [ | ||
self::CARD_HOLDER, | ||
self::CARD_NUMBER, | ||
self::CARD_CCV, | ||
]); | ||
} | ||
|
||
/** | ||
* @param mixed $value | ||
*/ | ||
public function process(string $key, $value): ?string | ||
{ | ||
if ($key === self::CARD_CCV) { | ||
// remove value | ||
return null; | ||
} | ||
|
||
if (!is_scalar($value)) { | ||
return $value; | ||
} | ||
|
||
$value = (string)$value; | ||
if ($key === self::CARD_NUMBER) { | ||
return substr($value, 0, 2) . '**-****-****-' . substr($value, -4); | ||
} | ||
|
||
if ($key === self::CARD_HOLDER) { | ||
$parts = explode(' ', $value); | ||
$newValue = ''; | ||
foreach ($parts as $part) { | ||
$newValue .= $part[0] . '**** '; | ||
} | ||
return trim($newValue); | ||
} | ||
|
||
throw NotSupportedType::key($key); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
<?php declare(strict_types=1); | ||
|
||
namespace Stefna\Logger\PII\Anonymizer; | ||
|
||
use Stefna\Logger\PII\Exception\NotSupportedType; | ||
|
||
final class PersonAnonymizer implements Anonymizer | ||
{ | ||
public const NAME = '_name'; | ||
public const PHONE = '_phone'; | ||
public const EMAIL = '_email'; | ||
public const SSN = '_ssn'; | ||
public const DOB = '_date_of_birth'; | ||
|
||
/** @var array<string, string> */ | ||
private $aliasFields = []; | ||
|
||
public function addAliasField(string $field, string $alias): void | ||
{ | ||
$this->aliasFields[$alias] = $field; | ||
} | ||
|
||
public function support(string $key): bool | ||
{ | ||
return in_array($key, [ | ||
self::NAME, | ||
self::PHONE, | ||
self::EMAIL, | ||
self::SSN, | ||
self::DOB, | ||
], true) || array_key_exists($key, $this->aliasFields); | ||
} | ||
|
||
/** | ||
* @param mixed $value | ||
*/ | ||
public function process(string $key, $value): ?string | ||
{ | ||
if (array_key_exists($key, $this->aliasFields)) { | ||
$key = $this->aliasFields[$key]; | ||
} | ||
|
||
if (in_array($key, [self::SSN, self::DOB], true)) { | ||
// remove value | ||
return null; | ||
} | ||
if (!is_scalar($value)) { | ||
return $value; | ||
} | ||
|
||
$value = (string)$value; | ||
if ($key === self::EMAIL) { | ||
$parts = explode('@', $value); | ||
$domain = array_pop($parts); | ||
$newValue = ''; | ||
foreach ($parts as $part) { | ||
$newValue .= $part[0] . '****'; | ||
} | ||
$newValue .= '@' . $domain; | ||
return $newValue; | ||
} | ||
if ($key === self::PHONE) { | ||
if (strlen($value) < 3) { | ||
return '****'; | ||
} | ||
return $value[0] . '****' . substr($value, -2); | ||
} | ||
if ($key === self::NAME) { | ||
$parts = explode(' ', $value); | ||
$newValue = ''; | ||
foreach ($parts as $part) { | ||
$newValue .= $part[0] . '**** '; | ||
} | ||
return trim($newValue); | ||
} | ||
|
||
throw NotSupportedType::key($key); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
<?php declare(strict_types=1); | ||
|
||
namespace Stefna\Logger\PII\Exception; | ||
|
||
final class NotSupportedType extends \RuntimeException | ||
{ | ||
public static function key(string $key): self | ||
{ | ||
return new self(sprintf('"%s" is not a valid key for anonymizer', $key)); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
<?php declare(strict_types=1); | ||
|
||
namespace Stefna\Logger\PII; | ||
|
||
use Stefna\Logger\PII\Anonymizer\CardAnonymizer; | ||
use Stefna\Logger\PII\Anonymizer\PersonAnonymizer; | ||
|
||
interface Fields | ||
{ | ||
public const CARD_NUMBER = CardAnonymizer::CARD_NUMBER; | ||
public const CARD_CCV = CardAnonymizer::CARD_CCV; | ||
public const CARD_HOLDER = CardAnonymizer::CARD_HOLDER; | ||
|
||
public const NAME = PersonAnonymizer::NAME; | ||
public const PHONE = PersonAnonymizer::PHONE; | ||
public const EMAIL = PersonAnonymizer::EMAIL; | ||
public const SSN = PersonAnonymizer::SSN; | ||
public const DOB = PersonAnonymizer::DOB; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
<?php declare(strict_types=1); | ||
|
||
namespace Stefna\Logger\PII; | ||
|
||
use Stefna\Logger\PII\Anonymizer\Anonymizer; | ||
use Stefna\Logger\PII\Anonymizer\CardAnonymizer; | ||
use Stefna\Logger\PII\Anonymizer\PersonAnonymizer; | ||
|
||
final class Processor | ||
{ | ||
/** @var Anonymizer[] */ | ||
private $anonymizers; | ||
|
||
public function __construct(Anonymizer ...$anonymizers) | ||
{ | ||
$this->anonymizers = $anonymizers; | ||
if (!$this->anonymizers) { | ||
$this->anonymizers[] = new CardAnonymizer(); | ||
$this->anonymizers[] = new PersonAnonymizer(); | ||
} | ||
} | ||
|
||
public function addAnonymizer(Anonymizer $anonymizer): void | ||
{ | ||
$this->anonymizers[] = $anonymizer; | ||
} | ||
|
||
/** | ||
* @param array{context: array<string, mixed>} $record | ||
* @return array{context: array<string, mixed>} | ||
*/ | ||
public function __invoke(array $record): array | ||
{ | ||
$record['context'] = $this->processContext($record['context']); | ||
|
||
return $record; | ||
} | ||
|
||
/** | ||
* @param array<string, mixed> $context | ||
* @return array<string, mixed> | ||
*/ | ||
private function processContext(array $context): array | ||
{ | ||
foreach ($context as $key => $value) { | ||
if (is_array($value)) { | ||
$context[$key] = $this->processContext($value); | ||
continue; | ||
} | ||
|
||
foreach ($this->anonymizers as $anonymizer) { | ||
if (!$anonymizer->support($key)) { | ||
continue; | ||
} | ||
$value = $anonymizer->process($key, $value); | ||
if ($value === null) { | ||
unset($context[$key]); | ||
continue 2; | ||
} | ||
$context[$key] = $value; | ||
} | ||
} | ||
return $context; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
<?php declare(strict_types=1); | ||
|
||
namespace Stefna\Logger\Processor; | ||
|
||
final class StripContextProcessor | ||
{ | ||
private const DEFAULT_FIELDS = [ | ||
'%password%', | ||
]; | ||
|
||
/** @var string[] */ | ||
private $fields = []; | ||
/** @var array<array-key, array{field: string, type: string}> */ | ||
private $wildCardFields = []; | ||
|
||
public function __construct(string ...$fields) | ||
{ | ||
$fields = $fields + self::DEFAULT_FIELDS; | ||
foreach ($fields as $field) { | ||
$this->addField($field); | ||
} | ||
} | ||
|
||
public function addField(string $field): void | ||
{ | ||
$wildcardCount = substr_count($field, '%'); | ||
if ($wildcardCount) { | ||
$wildcardField = [ | ||
'field' => strtolower(str_replace('%', '', $field)), | ||
'type' => $wildcardCount === 2 ? 'containing' : 'beginning', | ||
]; | ||
if ($wildcardCount === 1 && strpos($field, '%') === 0) { | ||
$wildcardField['type'] = 'ending'; | ||
} | ||
|
||
$this->wildCardFields[] = $wildcardField; | ||
} | ||
else { | ||
$this->fields[] = $field; | ||
} | ||
} | ||
|
||
/** | ||
* @param array{context: array<string, mixed>} $record | ||
* @return array{context: array<string, mixed>} | ||
*/ | ||
public function __invoke(array $record): array | ||
{ | ||
$record['context'] = $this->processContext($record['context']); | ||
|
||
return $record; | ||
} | ||
|
||
/** | ||
* @param array<string, mixed> $context | ||
* @return array<string, mixed> | ||
*/ | ||
private function processContext(array $context): array | ||
{ | ||
foreach ($context as $key => $value) { | ||
if (is_array($value)) { | ||
$context[$key] = $this->processContext($value); | ||
continue; | ||
} | ||
|
||
if (in_array($key, $this->fields, true)) { | ||
unset($context[$key]); | ||
continue; | ||
} | ||
|
||
$searchKey = strtolower($key); | ||
foreach ($this->wildCardFields as $field) { | ||
$pos = strpos($searchKey, $field['field']); | ||
if ($field['type'] === 'beginning' && $pos === 0) { | ||
unset($context[$key]); | ||
continue 2; | ||
} | ||
if ($field['type'] === 'containing' && $pos !== false) { | ||
unset($context[$key]); | ||
continue 2; | ||
} | ||
if ($pos !== false && | ||
$field['type'] === 'ending' && | ||
substr($searchKey, -strlen($field['field'])) === $field['field'] | ||
) { | ||
unset($context[$key]); | ||
continue 2; | ||
} | ||
} | ||
} | ||
return $context; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
<?php declare(strict_types=1); | ||
|
||
namespace Stefna\Logger\Handler; | ||
|
||
use PHPUnit\Framework\TestCase; | ||
use Stefna\Logger\PII\Anonymizer\CardAnonymizer; | ||
|
||
final class CardAnonymizerTest extends TestCase | ||
{ | ||
public function testCardNumber() | ||
{ | ||
$anonymizer = new CardAnonymizer(); | ||
|
||
$cardNumber = '1111-1111-1111-1234'; | ||
|
||
$this->assertSame( | ||
'11**-****-****-1234', | ||
$anonymizer->process(CardAnonymizer::CARD_NUMBER, $cardNumber) | ||
); | ||
} | ||
|
||
public function testCardCcv() | ||
{ | ||
$anonymizer = new CardAnonymizer(); | ||
|
||
$cardCcv = '1234'; | ||
|
||
$this->assertNull($anonymizer->process(CardAnonymizer::CARD_CCV, $cardCcv)); | ||
} | ||
|
||
public function testCardHolder() | ||
{ | ||
$anonymizer = new CardAnonymizer(); | ||
|
||
$cardHolder = 'Test Sub Last'; | ||
|
||
$this->assertSame( | ||
'T**** S**** L****', | ||
$anonymizer->process(CardAnonymizer::CARD_HOLDER, $cardHolder) | ||
); | ||
} | ||
} |
Oops, something went wrong.