Skip to content

Commit

Permalink
Updating Field Masker and adding additional tests
Browse files Browse the repository at this point in the history
  • Loading branch information
JustSteveKing committed Apr 4, 2024
1 parent efe56a8 commit 08fab51
Show file tree
Hide file tree
Showing 2 changed files with 96 additions and 108 deletions.
157 changes: 50 additions & 107 deletions src/Masking/FieldMasker.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,136 +6,79 @@

final class FieldMasker
{
/**
* Create a new instance of the FieldMasker.
* @param array<int,string|int|bool|array> $fields
*/
public function __construct(
public array $fields = [],
) {
}

/**
* Mask the inputted data.
* @param array<string,string|array> $data
* @return array
*/
public function mask(array $data): array
{
$collector = [];
foreach ($data as $key => $value) {
if (is_array($value)) {
$collector[$key] = $this->mask(
$collector[$key] = match (true) {
is_array($value) => $this->mask(
data: $value,
);
}
),
is_string($value) => $this->handleString(
key: $key,
value: $value,
),
default => $value,
};
}

if (is_bool($value) || is_int($value) || is_float($value) || is_null($value)) {
$collector[$key] = $value;
}
return $collector;
}

// we should know it is a string.
if (is_string($value)) {
// check if this is an auth header or api key header etc
// is the key a header we want to mask?
if ($this->isHeader(
name: $key,
)) {
// grab the sensitive part of the value and mask.
if ($this->isAuth(
value: $value,
)) {
$parts = explode(
separator: ' ',
string: $value,
);

if (count($parts) >= 2) {
for ($i = 1; $i < count($parts); $i++) {
$parts[$i] = $this->star(
string: $parts[$i]
);
}
} else {
$parts[0] = $this->star($parts[0]);
}

$value = implode(' ', $parts);
} else {
$value = $this->star(
string: $value,
);
}
}

if (in_array($key, $this->fields, true)) {
$collector[$key] = $this->star(
string: $value,
);
} else {
$collector[$key] = $value;
}
}
private function handleString(string $key, string $value): string
{
static $lowerFields = null;
if ($lowerFields === null) {
$lowerFields = array_map('strtolower', $this->fields);
}

return $collector;
$lowerKey = strtolower($key);

if (in_array($lowerKey, $lowerFields, true)) {
return $this->star($value);
}

if ($this->isSensitiveHeader($lowerKey)) {
return $this->maskAuthorization($value);
}

if ($this->isBase64($value)) {
return 'base64 encoded images are too big to process';
}

return $value;
}

/**
* Check if the field is a Header.
* @param int|bool|float|string|null $name
* @return bool
*/
private function isHeader(int|bool|float|null|string $name): bool
private function maskAuthorization(string $value): string
{
return in_array(
needle: $name,
haystack: [
'auth',
'Auth',
'Authorization',
'authorization',
'X-API-KEY',
'x-api-key',
],
strict: true,
);
$parts = explode(' ', $value, 2);
if (isset($parts[1])) {
$authTypeLower = strtolower($parts[0]);
if (in_array($authTypeLower, ['bearer', 'basic', 'digest'])) {
return $parts[0].' '.$this->star($parts[1]);
}
}

return $this->star($value);
}

/**
* Check is the value is part of an Auth header.
* @param string $value
* @return bool
*/
private function isAuth(string $value): bool
private function isSensitiveHeader(string $key): bool
{
return in_array(
needle: explode(
separator: ' ',
string: $value,
)[0],
haystack: [
'Bearer',
'bearer',
'Basic',
'basic',
],
strict: true,
);
return in_array($key, ['authorization', 'x-api-key'], true);
}

/**
* Replace a string input with a star.
* @param string $string
* @return string
*/
public function star(string $string): string
{
return str_repeat(
string: '*',
times: strlen(
string: $string,
),
);
return str_repeat('*', strlen($string));
}

private function isBase64(string $string): bool
{
return str_starts_with($string, 'data:image/') && str_contains($string, ';base64,');
}
}
47 changes: 46 additions & 1 deletion tests/Masking/FieldMaskerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@
'password' => '********',
'api_key' => '****',
],
'Authorization' => 'Bearer *************** ***',
'Authorization' => 'Bearer *******************',
'X-API-KEY' => '**************',
'cc' => '*******************',
'foo' => 'bar',
Expand Down Expand Up @@ -168,3 +168,48 @@
'foo' => 'bar',
]);
});

it('masks base64 encoded image strings', function () {
$masker = new FieldMasker();

$base64Image = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==';
$data = [
'image' => $base64Image,
];

$maskedData = $masker->mask($data);

// Assert that the base64 encoded image string is replaced with a mask or default value
expect($maskedData['image'])->not()->toEqual($base64Image);
expect($maskedData['image'])->toEqual('base64 encoded images are too big to process'); // Assuming 'DEFAULT_VALUE' is what you use for masking
});

it('does not mask non-base64 encoded strings', function () {
$masker = new FieldMasker();

$nonBase64String = 'This is a test string, not base64 encoded.';
$data = [
'description' => $nonBase64String,
];

$maskedData = $masker->mask($data);

// Assert that non-base64 encoded strings remain unchanged
expect($maskedData['description'])->toEqual($nonBase64String);
});

it('masks base64 encoded image strings within nested arrays', function () {
$masker = new FieldMasker();

$base64Image = 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJ...';
$data = [
'profile' => [
'avatar' => $base64Image,
],
];

$maskedData = $masker->mask($data);

// Assert that the base64 encoded image string in a nested array is masked
expect($maskedData)->toHaveKey('profile.avatar', 'base64 encoded images are too big to process');
});

0 comments on commit 08fab51

Please sign in to comment.