Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 35 additions & 28 deletions app/Services/Pii/PiiMatcherBaselineProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,25 +57,25 @@ private function governmentIdsGroup(): PiiMatcherGroupData
$this->make(
'national_id', 'government_ids', 'National ID / SSN', PiiSensitivity::Critical,
['/^(ssn|social[-_]?security|national[-_]?id|personal[-_]?id)$/i'],
$this->hash(),
$this->fake('numerify', ['###-##-####']),
'123-45-6789',
),
$this->make(
'passport_number', 'government_ids', 'Passport Number', PiiSensitivity::Critical,
['/^(passport|passport[-_]?number|passport[-_]?no)$/i'],
$this->hash(),
$this->fake('bothify', ['?########']),
'A12345678',
),
$this->make(
'drivers_license', 'government_ids', "Driver's License", PiiSensitivity::Critical,
['/^(drivers?[-_]?licen[sc]e|dl[-_]?number|license[-_]?number|driving[-_]?licen[sc]e)$/i'],
$this->hash(),
$this->fake('bothify', ['DL-#########']),
'DL-123456789',
),
$this->make(
'tax_id', 'government_ids', 'Tax ID / VAT Number', PiiSensitivity::Critical,
['/^(tax[-_]?id|tax[-_]?number|tin|vat[-_]?number|vat[-_]?id|fiscal[-_]?id)$/i'],
$this->hash(),
$this->fake('numerify', ['##-#######']),
'12-3456789',
),
],
Expand Down Expand Up @@ -241,25 +241,25 @@ private function financialGroup(): PiiMatcherGroupData
$this->make(
'credit_card', 'financial', 'Credit Card Number', PiiSensitivity::Critical,
['/^(credit[-_]?card|card[-_]?number|cc[-_]?number|payment[-_]?card|pan)$/i'],
$this->mask(4, '*', false),
$this->fake('creditCardNumber'),
'4242424242424242',
),
$this->make(
'iban', 'financial', 'IBAN / Bank Account', PiiSensitivity::Critical,
['/^(iban|bank[-_]?account|kontonummer)$/i'],
$this->mask(4, '*', false),
$this->fake('iban'),
'DE89370400440532013000',
),
$this->make(
'bank_routing_number', 'financial', 'Bank Routing / Sort Code', PiiSensitivity::Critical,
['/^(routing[-_]?number|sort[-_]?code|aba[-_]?number|bank[-_]?code|bic|swift[-_]?code)$/i'],
$this->hash(),
$this->fake('numerify', ['#########']),
'021000021',
),
$this->make(
'crypto_wallet', 'financial', 'Crypto Wallet Address', PiiSensitivity::High,
['/^(wallet[-_]?address|crypto[-_]?wallet|bitcoin[-_]?address|eth[-_]?address|blockchain[-_]?address)$/i'],
$this->hash(),
$this->fake('sha256'),
'1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa',
enabled: false,
),
Expand All @@ -283,19 +283,19 @@ private function medicalGroup(): PiiMatcherGroupData
$this->make(
'medical_record_id', 'medical', 'Medical Record Number', PiiSensitivity::Critical,
['/^(medical[-_]?record|mrn|medical[-_]?record[-_]?number|chart[-_]?number)$/i'],
$this->hash(),
$this->fake('bothify', ['MRN-######']),
'MRN-789456',
),
$this->make(
'health_insurance_id', 'medical', 'Health Insurance ID', PiiSensitivity::Critical,
['/^(insurance[-_]?id|insurance[-_]?number|health[-_]?plan|policy[-_]?number|member[-_]?id)$/i'],
$this->hash(),
$this->fake('bothify', ['INS-#########']),
'INS-123456789',
),
$this->make(
'patient_id', 'medical', 'Patient ID', PiiSensitivity::Critical,
['/^(patient[-_]?id|patient[-_]?number|patient[-_]?reference)$/i'],
$this->hash(),
$this->fake('bothify', ['PAT-######']),
'PAT-001234',
enabled: false,
),
Expand Down Expand Up @@ -326,13 +326,13 @@ private function biometricGroup(): PiiMatcherGroupData
$this->make(
'biometric_data', 'biometric', 'Biometric Identifier', PiiSensitivity::Critical,
['/^(fingerprint|face[-_]?(id|encoding|hash|vector)|voice[-_]?print|biometric|retina[-_]?scan)$/i'],
$this->hash(),
$this->nullify(),
'base64-biometric-hash',
),
$this->make(
'dna_profile', 'biometric', 'DNA / Genetic Data', PiiSensitivity::Critical,
['/^(dna|dna[-_]?profile|genetic[-_]?data|genome)$/i'],
$this->hash(),
$this->nullify(),
'ATCGATCGATCG',
enabled: false,
),
Expand Down Expand Up @@ -377,25 +377,25 @@ private function digitalIdentityGroup(): PiiMatcherGroupData
$this->make(
'ip_address', 'digital_identity', 'IP Address', PiiSensitivity::Medium,
['/^(ip|ip[-_]?addr(ess)?|client[-_]?ip|remote[-_]?ip|user[-_]?ip)$/i'],
$this->mask(0, '*', true),
$this->fake('ipv4'),
'192.168.1.100',
),
$this->make(
'device_id', 'digital_identity', 'Device ID / UDID', PiiSensitivity::Medium,
['/^(device[-_]?id|device[-_]?identifier|udid|imei|device[-_]?token)$/i'],
$this->hash(),
$this->fake('uuid'),
'550e8400-e29b-41d4-a716-446655440000',
),
$this->make(
'session_id', 'digital_identity', 'Session ID', PiiSensitivity::High,
['/^(session[-_]?id|session[-_]?token|session[-_]?key)$/i'],
$this->hash(),
$this->nullify(),
'sess_abc123xyz789',
),
$this->make(
'mac_address', 'digital_identity', 'MAC Address', PiiSensitivity::Medium,
['/^(mac[-_]?address|mac[-_]?addr|hardware[-_]?address|physical[-_]?address)$/i'],
$this->hash(),
$this->fake('macAddress'),
'00:1A:2B:3C:4D:5E',
),
$this->make(
Expand All @@ -418,26 +418,26 @@ private function authenticationGroup(): PiiMatcherGroupData
$this->make(
'password', 'authentication', 'Password / Secret', PiiSensitivity::Critical,
['/^(password|passwd|pwd|secret|passwort)$/i'],
$this->hash(),
$this->staticValue('REDACTED'),
'mysecretpassword',
),
$this->make(
'oauth_token', 'authentication', 'OAuth / Access Token', PiiSensitivity::Critical,
['/^(oauth[-_]?token|access[-_]?token|refresh[-_]?token|bearer[-_]?token|auth[-_]?token)$/i'],
$this->hash(),
$this->staticValue('REDACTED'),
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9',
),
$this->make(
'api_token', 'authentication', 'API Token / Key', PiiSensitivity::Critical,
['/^(token|api[-_]?key)$/i'],
$this->hash(),
$this->staticValue('REDACTED'),
'sk_live_abc123xyz789',
enabled: false,
),
$this->make(
'private_key', 'authentication', 'Private / Encryption Key', PiiSensitivity::Critical,
['/^(private[-_]?key|secret[-_]?key|signing[-_]?key|encryption[-_]?key)$/i'],
$this->hash(),
$this->staticValue('REDACTED'),
'-----BEGIN RSA PRIVATE KEY-----',
enabled: false,
),
Expand Down Expand Up @@ -494,7 +494,14 @@ private function fake(string $method, array $args = []): ColumnCloningConfigData
);
}

private function hash(string $algorithm = 'sha256', string $salt = ''): ColumnCloningConfigData
/**
* Build a hash transformation. Salt defaults to null so the engine's
* per-run random salt is applied at transform time — required for
* pseudonymization that defeats cross-run linkability (GDPR Art. 4 Nr. 5).
* Pass an explicit salt only if reproducible hashes across runs are
* required (e.g. fixture data).
*/
private function hash(string $algorithm = 'sha256', ?string $salt = null): ColumnCloningConfigData
{
return new ColumnCloningConfigData(
columnName: '',
Expand All @@ -510,19 +517,19 @@ private function hash(string $algorithm = 'sha256', string $salt = ''): ColumnCl
);
}

private function mask(int $visibleChars = 0, string $maskChar = '*', bool $preserveFormat = false): ColumnCloningConfigData
private function staticValue(string $value): ColumnCloningConfigData
{
return new ColumnCloningConfigData(
columnName: '',
strategy: 'mask',
strategy: 'static',
fakerMethod: null,
fakerArguments: [],
hashAlgorithm: null,
hashSalt: null,
maskChar: $maskChar,
visibleChars: $visibleChars,
preserveFormat: $preserveFormat,
staticValue: null,
maskChar: null,
visibleChars: null,
preserveFormat: null,
staticValue: $value,
);
}

Expand Down
36 changes: 22 additions & 14 deletions tests/Feature/Commands/Matchers/CheckCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,11 @@
it('shows transformation details for a matched column', function (): void {
Storage::fake('local');

$this->artisan('matchers:check', ['column' => 'password'])
->expectsOutputToContain('password')
// employee_id keeps the hash strategy because it is often used as a
// referential identifier inside the same run. The per-run random salt
// applied by the engine defeats cross-run linkability.
$this->artisan('matchers:check', ['column' => 'employee_id'])
->expectsOutputToContain('employee_id')
->expectsOutputToContain('hash')
->expectsOutputToContain('sha256')
->assertExitCode(ExitCode::Success->value);
Expand Down Expand Up @@ -76,20 +79,23 @@
it('shows built-in example for credit card column', function (): void {
Storage::fake('local');

// credit_card is now anonymized via Faker (`creditCardNumber`), so the
// generated output is randomized — assert only that the input is shown
// and that Faker is the strategy used.
$this->artisan('matchers:check', ['column' => 'credit_card'])
->expectsOutputToContain('Example:')
->expectsOutputToContain('Input: 4242424242424242')
->expectsOutputToContain('Output: 4242************')
->expectsOutputToContain('faker generates fresh data')
->assertExitCode(ExitCode::Success->value);
});

it('shows built-in example for ip_address column with format preservation', function (): void {
it('shows built-in example for ip_address column', function (): void {
Storage::fake('local');

$this->artisan('matchers:check', ['column' => 'ip'])
->expectsOutputToContain('Example:')
->expectsOutputToContain('Input: 192.168.1.100')
->expectsOutputToContain('Output: ***.***.*.***')
->expectsOutputToContain('faker generates fresh data')
->assertExitCode(ExitCode::Success->value);
});

Expand All @@ -99,17 +105,17 @@
$this->artisan('matchers:check', ['column' => 'iban'])
->expectsOutputToContain('Example:')
->expectsOutputToContain('Input: DE89370400440532013000')
->expectsOutputToContain('Output: DE89******************')
->expectsOutputToContain('faker generates fresh data')
->assertExitCode(ExitCode::Success->value);
});

it('shows built-in example for hash strategy (password)', function (): void {
it('shows static REDACTED output for password column', function (): void {
Storage::fake('local');

$this->artisan('matchers:check', ['column' => 'password'])
->expectsOutputToContain('Example:')
->expectsOutputToContain('Input: mysecretpassword')
->expectsOutputToContain('Output: ')
->expectsOutputToContain('Output: REDACTED')
->assertExitCode(ExitCode::Success->value);
});

Expand All @@ -128,18 +134,20 @@

$this->artisan('matchers:check', ['column' => 'credit_card', 'value' => '5555555555554444'])
->expectsOutputToContain('Input: 5555555555554444')
->expectsOutputToContain('Output: 5555************')
->expectsOutputToContain('faker generates fresh data')
->assertExitCode(ExitCode::Success->value);
});

it('applies user-provided value to hash strategy', function (): void {
Storage::fake('local');

$expected = hash('sha256', 'mypassword');

$this->artisan('matchers:check', ['column' => 'password', 'value' => 'mypassword'])
->expectsOutputToContain('Input: mypassword')
->expectsOutputToContain(sprintf('Output: %s', $expected))
// employee_id uses hash strategy with the engine's per-run random salt,
// so the exact output is not deterministic across runs; assert only the
// input echo and that the matcher details show a 64-char sha256 prefix.
$this->artisan('matchers:check', ['column' => 'employee_id', 'value' => 'EMP-42'])
->expectsOutputToContain('Input: EMP-42')
->expectsOutputToContain('Output: ')
->expectsOutputToContain('algorithm: sha256')
->assertExitCode(ExitCode::Success->value);
});

Expand Down