Skip to content

Commit

Permalink
Merge pull request #1 from stefnadev/feature/pii-anonymizer
Browse files Browse the repository at this point in the history
Add support for handling anonyimze some pii data
  • Loading branch information
sunkan authored Jun 3, 2021
2 parents f45c2dc + a40b41b commit 949b9e5
Show file tree
Hide file tree
Showing 11 changed files with 617 additions and 0 deletions.
14 changes: 14 additions & 0 deletions src/PII/Anonymizer/Anonymizer.php
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);
}
52 changes: 52 additions & 0 deletions src/PII/Anonymizer/CardAnonymizer.php
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);
}
}
79 changes: 79 additions & 0 deletions src/PII/Anonymizer/PersonAnonymizer.php
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);
}
}
11 changes: 11 additions & 0 deletions src/PII/Exception/NotSupportedType.php
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));
}
}
19 changes: 19 additions & 0 deletions src/PII/Fields.php
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;
}
65 changes: 65 additions & 0 deletions src/PII/Processor.php
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;
}
}
93 changes: 93 additions & 0 deletions src/Processor/StripContextProcessor.php
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;
}
}
42 changes: 42 additions & 0 deletions tests/Handler/CardAnonymizerTest.php
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)
);
}
}
Loading

0 comments on commit 949b9e5

Please sign in to comment.