Skip to content

Commit 91f2857

Browse files
authored
PII Detection, Partial Masking, Stats API, and PHP 8.5 Support (#62)
Adds powerful new capabilities for PII detection, introduces partial masking, and provides a statistics API for monitoring scrubbing activity. ## New Features ### 5 New PII Detection Patterns | Pattern | Example | Masked Output | |---------|---------|---------------| | SSN | `123-45-6789` | `***-**-****` | | Phone | `+1 (555) 123-4567` | `(***) ***-****` | | IPv4 | `192.168.1.1` | `***.***.***.***` | | IPv6 | `2001:0db8:...` | `****:****:...` | | IBAN | `GB82WEST1234...` | `********************` | ### Partial Masking Support New patterns use contextual replacement values via `getReplacementValue()` method: ```php // Before: everything becomes "**redacted**" // After: SSN → "***-**-****", Phone → "(***) ***-****" ``` ### Detection Statistics API ```php // Get stats for current request $stats = Scrubber::getStats(); // ['total_scrubs' => 5, 'patterns_matched' => ['JsonWebToken' => 2, 'EmailAddress' => 3]] // Test what patterns match $result = Scrubber::test('Contact: john@example.com, SSN: 123-45-6789'); // ['matched' => true, 'patterns' => ['EmailAddress' => 1, 'SocialSecurityNumber' => 1], 'scrubbed' => '...'] // Reset stats Scrubber::resetStats(); ``` ### PHP 8.5 Support - Added PHP 8.5 to composer.json constraints - Added PHP 8.5 to CI test matrix ## Performance Fixes (#61) - Optimized regex repository access pattern - Changed from `Collection::each()` to native `foreach` loop - Cached config values instead of calling per-pattern
1 parent 067f5cf commit 91f2857

File tree

16 files changed

+500
-42
lines changed

16 files changed

+500
-42
lines changed

.github/workflows/phpunit.yml

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ jobs:
1111
strategy:
1212
fail-fast: false
1313
matrix:
14-
php: [8.2, 8.3, 8.4]
14+
php: [8.2, 8.3, 8.4, 8.5]
1515
laravel: ['10.*', '11.*', '12.*']
1616
include:
1717
- php: 8.2
@@ -46,11 +46,21 @@ jobs:
4646
laravel: 12.*
4747
testbench: 10.*
4848

49+
- php: 8.5
50+
laravel: 11.*
51+
testbench: 9.*
52+
53+
- php: 8.5
54+
laravel: 12.*
55+
testbench: 10.*
56+
4957
exclude:
5058
- laravel: 12.*
5159
php: 8.1
5260
- laravel: 10.*
5361
php: 8.4
62+
- laravel: 10.*
63+
php: 8.5
5464

5565
name: PHP${{ matrix.php }} - Laravel ${{ matrix.laravel }}${{ matrix.skip && ' (skipped)' || '' }}
5666

README.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@
1919
A Laravel package to scrub sensitive information that breaks operational security policies from being leaked on
2020
accident ~~_or not_~~ by developers.
2121

22+
## Requirements
23+
24+
- PHP 8.1, 8.2, 8.3, 8.4, or 8.5
25+
- Laravel 10.x, 11.x, or 12.x
26+
2227
## Installation
2328

2429
install the package via composer:
@@ -171,6 +176,27 @@ Scrubber::processMessage('<insert jwt token here>');
171176
// **redacted**
172177
```
173178

179+
### Detection Statistics API
180+
181+
Track what patterns are matching and how often:
182+
183+
```php
184+
// Get scrubbing statistics for the current request
185+
$stats = Scrubber::getStats();
186+
// ['total_scrubs' => 5, 'patterns_matched' => ['JsonWebToken' => 2, 'EmailAddress' => 3]]
187+
188+
// Test a string without modifying stats - useful for debugging
189+
$result = Scrubber::test('Contact: john@example.com, SSN: 123-45-6789');
190+
// [
191+
// 'matched' => true,
192+
// 'patterns' => ['EmailAddress' => 1, 'SocialSecurityNumber' => 1],
193+
// 'scrubbed' => 'Contact: **redacted**, SSN: ***-**-****'
194+
// ]
195+
196+
// Reset statistics between requests
197+
Scrubber::resetStats();
198+
```
199+
174200
## Log Channel Opt-in
175201

176202
This package provides you the ability to define through the configuration file what channels you want to scrub
@@ -215,6 +241,28 @@ class.
215241
],
216242
```
217243

244+
> **Note**: The package includes 31 built-in patterns. See all available patterns in [RegexCollection.php](https://github.com/YorCreative/Laravel-Scrubber/blob/main/src/Repositories/RegexCollection.php).
245+
246+
### PII Detection with Partial Masking
247+
248+
The following patterns use contextual replacement values for improved readability instead of the generic `**redacted**`:
249+
250+
| Pattern | Detects | Masked Output |
251+
|---------|---------|---------------|
252+
| `RegexCollection::$SOCIAL_SECURITY_NUMBER` | US Social Security Numbers | `***-**-****` |
253+
| `RegexCollection::$PHONE_NUMBER` | Phone numbers (US/International) | `(***) ***-****` |
254+
| `RegexCollection::$IP_ADDRESS_V4` | IPv4 addresses | `***.***.***.***` |
255+
| `RegexCollection::$IP_ADDRESS_V6` | IPv6 addresses | `****:****:****:...` |
256+
| `RegexCollection::$IBAN` | International Bank Account Numbers | `********************` |
257+
258+
```php
259+
Scrubber::processMessage('SSN: 123-45-6789, Phone: (555) 123-4567');
260+
// "SSN: ***-**-****, Phone: (***) ***-****"
261+
262+
Scrubber::processMessage('Server IP: 192.168.1.1');
263+
// "Server IP: ***.***.***.***"
264+
```
265+
218266
### Opting Into Custom Extended Classes
219267

220268
> To create custom scrubbers, see the [Extending the Scrubber](#extending-the-scrubber) section.

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
],
1717
"minimum-stability": "dev",
1818
"require": {
19-
"php": "^8.1|^8.2|^8.3",
19+
"php": "^8.1|^8.2|^8.3|^8.4|^8.5",
2020
"illuminate/contracts": "^9.0|^10.0|^11.0|^12.0",
2121
"monolog/monolog": "^2.0|^3",
2222
"guzzlehttp/guzzle": "^7.5",

phpunit.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
<testsuite name="Feature">
88
<directory suffix="Test.php">./tests/Feature</directory>
99
</testsuite>
10+
<testsuite name="Performance">
11+
<directory suffix="Test.php">./tests/Performance</directory>
12+
</testsuite>
1013
</testsuites>
1114
<php>
1215
<env name="APP_ENV" value="testing"/>

src/RegexCollection/Iban.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
namespace YorCreative\Scrubber\RegexCollection;
4+
5+
use YorCreative\Scrubber\Interfaces\RegexCollectionInterface;
6+
7+
class Iban implements RegexCollectionInterface
8+
{
9+
public function getPattern(): string
10+
{
11+
// Matches IBAN: 2 letter country code + 2 check digits + up to 30 alphanumeric characters
12+
return '\b[A-Z]{2}\d{2}[A-Z0-9]{4,30}\b';
13+
}
14+
15+
public function getTestableString(): string
16+
{
17+
// Test IBAN (GB format with zeros)
18+
return 'GB00TEST00000000000000';
19+
}
20+
21+
public function isSecret(): bool
22+
{
23+
return false;
24+
}
25+
26+
public function getReplacementValue(): string
27+
{
28+
return '********************';
29+
}
30+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
namespace YorCreative\Scrubber\RegexCollection;
4+
5+
use YorCreative\Scrubber\Interfaces\RegexCollectionInterface;
6+
7+
class IpAddressV4 implements RegexCollectionInterface
8+
{
9+
public function getPattern(): string
10+
{
11+
// Matches IPv4 addresses: 192.168.1.1, 10.0.0.1, etc.
12+
return '\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b';
13+
}
14+
15+
public function getTestableString(): string
16+
{
17+
return '192.168.0.1';
18+
}
19+
20+
public function isSecret(): bool
21+
{
22+
return false;
23+
}
24+
25+
public function getReplacementValue(): string
26+
{
27+
return '***.***.***.***';
28+
}
29+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
namespace YorCreative\Scrubber\RegexCollection;
4+
5+
use YorCreative\Scrubber\Interfaces\RegexCollectionInterface;
6+
7+
class IpAddressV6 implements RegexCollectionInterface
8+
{
9+
public function getPattern(): string
10+
{
11+
// Matches IPv6 addresses including compressed forms
12+
return '\b(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}\b|\b(?:[0-9a-fA-F]{1,4}:){1,7}:\b|\b(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}\b|\b(?:[0-9a-fA-F]{1,4}:){1,5}(?::[0-9a-fA-F]{1,4}){1,2}\b|\b(?:[0-9a-fA-F]{1,4}:){1,4}(?::[0-9a-fA-F]{1,4}){1,3}\b|\b::(?:[0-9a-fA-F]{1,4}:){0,5}[0-9a-fA-F]{1,4}\b|\b[0-9a-fA-F]{1,4}::(?:[0-9a-fA-F]{1,4}:){0,5}[0-9a-fA-F]{1,4}\b';
13+
}
14+
15+
public function getTestableString(): string
16+
{
17+
return '2001:0db8:85a3:0000:0000:8a2e:0370:7334';
18+
}
19+
20+
public function isSecret(): bool
21+
{
22+
return false;
23+
}
24+
25+
public function getReplacementValue(): string
26+
{
27+
return '****:****:****:****:****:****:****:****';
28+
}
29+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
namespace YorCreative\Scrubber\RegexCollection;
4+
5+
use YorCreative\Scrubber\Interfaces\RegexCollectionInterface;
6+
7+
class PhoneNumber implements RegexCollectionInterface
8+
{
9+
public function getPattern(): string
10+
{
11+
// Matches various phone formats:
12+
// +1 (555) 123-4567, 555-123-4567, (555) 123-4567, 5551234567, +1-555-123-4567
13+
return '(?:\+?1[-.\s]?)?(?:\(?\d{3}\)?[-.\s]?)?\d{3}[-.\s]?\d{4}\b';
14+
}
15+
16+
public function getTestableString(): string
17+
{
18+
return '+1 (555) 000-0000';
19+
}
20+
21+
public function isSecret(): bool
22+
{
23+
return false;
24+
}
25+
26+
public function getReplacementValue(): string
27+
{
28+
return '(***) ***-****';
29+
}
30+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
namespace YorCreative\Scrubber\RegexCollection;
4+
5+
use YorCreative\Scrubber\Interfaces\RegexCollectionInterface;
6+
7+
class SocialSecurityNumber implements RegexCollectionInterface
8+
{
9+
public function getPattern(): string
10+
{
11+
// Matches SSN formats: 123-45-6789, 123 45 6789, 123456789
12+
return '\b(?!000|666|9\d{2})\d{3}[-\s]?(?!00)\d{2}[-\s]?(?!0000)\d{4}\b';
13+
}
14+
15+
public function getTestableString(): string
16+
{
17+
// Using obviously fake SSN for tests - 123-45-6789 is a well-known test value
18+
// This matches the regex pattern but is universally recognized as fake
19+
return '123-45-6789';
20+
}
21+
22+
public function isSecret(): bool
23+
{
24+
return false;
25+
}
26+
27+
public function getReplacementValue(): string
28+
{
29+
return '***-**-****';
30+
}
31+
}

src/Repositories/RegexCollection.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,14 @@ class RegexCollection
5555
public static string $TWILIO_APP_SID = 'TwilioAppSid';
5656

5757
public static string $EMAIL_ADDRESS = 'EmailAddress';
58+
59+
public static string $SOCIAL_SECURITY_NUMBER = 'SocialSecurityNumber';
60+
61+
public static string $PHONE_NUMBER = 'PhoneNumber';
62+
63+
public static string $IP_ADDRESS_V4 = 'IpAddressV4';
64+
65+
public static string $IP_ADDRESS_V6 = 'IpAddressV6';
66+
67+
public static string $IBAN = 'Iban';
5868
}

0 commit comments

Comments
 (0)