Skip to content

Commit

Permalink
Dumper, BlueScreen: added Scrubber (#439)
Browse files Browse the repository at this point in the history
  • Loading branch information
dakujem authored and dg committed Dec 9, 2020
1 parent f4c516c commit c2558cb
Show file tree
Hide file tree
Showing 6 changed files with 180 additions and 1 deletion.
4 changes: 4 additions & 0 deletions src/Tracy/BlueScreen/BlueScreen.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ class BlueScreen
/** @var int */
public $maxLength = 150;

/** @var callable|null a callable returning true for sensitive data; fn(string $key, mixed $val): bool */
public $scrubber;

/** @var string[] */
public $keysToHide = ['password', 'passwd', 'pass', 'pwd', 'creditcard', 'credit card', 'cc', 'pin'];

Expand Down Expand Up @@ -384,6 +387,7 @@ public function getDumper(): \Closure
Dumper::TRUNCATE => $this->maxLength,
Dumper::SNAPSHOT => &$this->snapshot,
Dumper::LOCATION => Dumper::LOCATION_CLASS,
Dumper::SCRUBBER => $this->scrubber,
Dumper::KEYS_TO_HIDE => $this->keysToHide,
], $k);
};
Expand Down
13 changes: 12 additions & 1 deletion src/Tracy/Dumper/Describer.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ final class Describer
/** @var array */
public $keysToHide = [];

/** @var callable|null fn(string $key, mixed $val): bool */
public $scrubber;

/** @var bool */
public $location = false;

Expand Down Expand Up @@ -78,7 +81,7 @@ public function describe($var, $key = null): \stdClass
private function describeVar($var, int $depth = 0, int $refId = null, $key = null)
{
switch (true) {
case is_string($key) && isset($this->keysToHide[strtolower($key)]):
case is_string($key) && $this->isSensitive($key, $var):
return $this->hideValue($var);
case $var === null:
case is_bool($var):
Expand Down Expand Up @@ -260,6 +263,14 @@ private function exposeObject(object $obj, Value $value): ?array
}


private function isSensitive(string $k, $v = null): bool
{
return
($this->scrubber !== null && ($this->scrubber)($k, $v)) ||
isset($this->keysToHide[strtolower($k)]);
}


private function hideValue($var): Value
{
$s = self::HIDDEN_VALUE . ' (' . (is_object($var) ? Helpers::getClass($var) : gettype($var)) . ')';
Expand Down
6 changes: 6 additions & 0 deletions src/Tracy/Dumper/Dumper.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ class Dumper
SNAPSHOT = 'snapshot', // array used for shared snapshot for lazy-loading via JavaScript
DEBUGINFO = 'debuginfo', // use magic method __debugInfo if exists (defaults to false)
KEYS_TO_HIDE = 'keystohide', // sensitive keys not displayed (defaults to [])
SCRUBBER = 'scrubber', // detects sensitive keys not to be displayed
THEME = 'theme'; // color theme (defaults to light)

public const
Expand Down Expand Up @@ -93,6 +94,9 @@ class Dumper
/** @var bool display location by dump()? */
public static $showLocation;

/** @var callable|null detects sensitive keys not to be displayed by dump() */
public static $scrubber = null;

/** @var array sensitive keys not displayed by dump() */
public static $keysToHide = [];

Expand Down Expand Up @@ -150,6 +154,7 @@ private static function fromStatics(): array
self::ITEMS => self::$maxItems,
self::LOCATION => Debugger::$showLocation ?? self::$showLocation,
self::KEYS_TO_HIDE => self::$keysToHide,
self::SCRUBBER => self::$scrubber,
self::THEME => self::$theme,
];
}
Expand Down Expand Up @@ -218,6 +223,7 @@ private function __construct(array $options = [])
$describer->maxLength = $options[self::TRUNCATE] ?? $describer->maxLength;
$describer->maxItems = $options[self::ITEMS] ?? $describer->maxItems;
$describer->debugInfo = $options[self::DEBUGINFO] ?? $describer->debugInfo;
$describer->scrubber = $options[self::SCRUBBER] ?? $describer->scrubber;
$describer->keysToHide = array_flip(array_map('strtolower', $options[self::KEYS_TO_HIDE] ?? []));
$describer->resourceExposers = ($options['resourceExporters'] ?? []) + self::$resources;
$describer->objectExposers = ($options[self::OBJECT_EXPORTERS] ?? []) + self::$objectExporters;
Expand Down
50 changes: 50 additions & 0 deletions tests/Tracy/BlueScreen.getDumper().phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

declare(strict_types=1);


use Tester\Assert;

require __DIR__ . '/../bootstrap.php';


test('dumper with default keysToHide scrubbing', function () {
$blueScreen = new Tracy\BlueScreen;
$dumper = $blueScreen->getDumper();
Assert::contains('foo', $dumper('foo', 'bar'));
Assert::notContains('secret', $dumper('secret', 'password'));
Assert::notContains('secret', $dumper('secret', 'PiN'));
});

test('dumper with custom scrubbing', function () {
$blueScreen = new Tracy\BlueScreen;
$blueScreen->scrubber = function (string $k, $v = null): bool {
return strtolower($k) === 'pin' || strtolower($k) === 'foo' || $v === 42;
};
$dumper = $blueScreen->getDumper();
Assert::contains('foo', $dumper('foo', 'bar'));
Assert::notContains('secret', $dumper('secret', 'password')); // default keysToHide

Assert::notContains('secret', $dumper('secret', 'PiN')); // scrubbed by key
Assert::notContains('42', $dumper(42, 'bar')); // scrubbed by value
});

test('dumper with regexp scrubbing', function () {
$blueScreen = new Tracy\BlueScreen;
$blueScreen->scrubber = function (string $k): bool {
return (bool) preg_match('#password#i', $k);
};
$dumper = $blueScreen->getDumper();
Assert::contains('foo', $dumper('foo', 'bar'));
Assert::notContains('secret', $dumper('secret', 'super_password'));

$fix = [
'password' => 'secret',
'foo' => 'foo ok',
'password_check' => 'secret',
'DATABASE_PASSWORD' => 'secret',
];

Assert::notContains('secret', $dumper($fix));
Assert::contains('foo ok', $dumper($fix));
});
44 changes: 44 additions & 0 deletions tests/Tracy/Dumper.dump().cli.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,47 @@ test('returned value', function () {
$obj = new stdClass;
Assert::same(Dumper::dump($obj), $obj);
});

test('with custom scrubber', function () {
// this test could be placed in a separate file,
// but then `Capture` class and stream setup would have to be copied or refactored to a separate file
$obj = (object) [
'a' => 456,
'password' => 'secret1',
'PASSWORD' => 'secret2',
'Pin' => 'secret3',
'foo' => 'bar',
'q' => 42,
'inner' => [
'a' => 123,
'password' => 'secret4',
'PASSWORD' => 'secret5',
'Pin' => 'secret6',
'bar' => 42,
],
];
$scrubber = function (string $k, $v = null): bool {
return strtolower($k) === 'pin' || strtolower($k) === 'foo' || $v === 42;
};
$expect = <<<'XX'
stdClass #%d%
a: 456
password: 'secret1'
PASSWORD: 'secret2'
Pin: ***** (string)
foo: ***** (string)
q: ***** (integer)
inner: array (5)
| 'a' => 123
| 'password' => 'secret4'
| 'PASSWORD' => 'secret5'
| 'Pin' => ***** (string)
| 'bar' => ***** (integer)
XX;
Dumper::$useColors = false;
Capture::$buffer = '';

Dumper::$scrubber = $scrubber;
Dumper::dump($obj);
Assert::match($expect, Capture::$buffer);
});
64 changes: 64 additions & 0 deletions tests/Tracy/Dumper.scrubber.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?php

declare(strict_types=1);

use Tester\Assert;
use Tracy\Dumper;


require __DIR__ . '/../bootstrap.php';


$obj = (object) [
'a' => 456,
'password' => 'secret1',
'PASSWORD' => 'secret2',
'Pin' => 'secret3',
'foo' => 'bar',
'q' => 42,
'inner' => [
'a' => 123,
'password' => 'secret4',
'PASSWORD' => 'secret5',
'Pin' => 'secret6',
'bar' => 42,
],
];
$scrubber = function (string $k, $v = null): bool {
return strtolower($k) === 'pin' || strtolower($k) === 'foo' || $v === 42;
};
$expect1 = <<<'XX'
stdClass #%d%
a: 456
password: 'secret1'
PASSWORD: 'secret2'
Pin: ***** (string)
foo: ***** (string)
q: ***** (integer)
inner: array (5)
| 'a' => 123
| 'password' => 'secret4'
| 'PASSWORD' => 'secret5'
| 'Pin' => ***** (string)
| 'bar' => ***** (integer)
XX;

Assert::match($expect1, Dumper::toText($obj, [Dumper::SCRUBBER => $scrubber]));

// scrubber works with "keys to hide" (back compatibility)
$expect2 = <<<'XX'
stdClass #%d%
a: 456
password: ***** (string)
PASSWORD: ***** (string)
Pin: ***** (string)
foo: ***** (string)
q: ***** (integer)
inner: array (5)
| 'a' => 123
| 'password' => ***** (string)
| 'PASSWORD' => ***** (string)
| 'Pin' => ***** (string)
| 'bar' => ***** (integer)
XX;
Assert::match($expect2, Dumper::toText($obj, [Dumper::SCRUBBER => $scrubber, Dumper::KEYS_TO_HIDE => ['password']]));

0 comments on commit c2558cb

Please sign in to comment.