TenantAtlas/app/Services/Intune/SecretClassificationService.php
ahmido 45a804970e feat: complete admin canonical tenant rollout (#165)
## Summary
- complete Spec 136 canonical admin tenant rollout across admin-visible and shared Filament surfaces
- add the shared panel-aware tenant resolver helper, persisted filter-state synchronization, and admin navigation segregation for tenant-sensitive resources
- expand regression, guard, and parity coverage for admin-path tenant resolution, stale filters, workspace-wide tenant-default surfaces, and panel split behavior

## Validation
- `vendor/bin/sail artisan test --compact tests/Feature/Guards/AdminTenantResolverGuardTest.php`
- `vendor/bin/sail artisan test --compact tests/Feature/Filament/TableStatePersistenceTest.php`
- `vendor/bin/sail artisan test --compact --filter='CanonicalAdminTenantFilterState|PolicyResource|BackupSchedule|BackupSet|FindingResource|BaselineCompareLanding|RestoreRunResource|InventoryItemResource|PolicyVersionResource|ProviderConnectionResource|TenantDiagnostics|InventoryCoverage|InventoryKpiHeader|AuditLog|EntraGroup'`
- `vendor/bin/sail bin pint --dirty --format agent`

## Notes
- Livewire v4.0+ compliance is preserved with Filament v5.
- Provider registration remains unchanged in `bootstrap/providers.php`.
- `PolicyResource` and `PolicyVersionResource` have admin global search disabled explicitly; `EntraGroupResource` keeps admin-aware scoped search with a View page.
- Destructive and governance-sensitive actions retain existing confirmation and authorization behavior while using canonical tenant parity.
- No new assets were introduced, so deployment asset strategy is unchanged and does not add new `filament:assets` work.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #165
2026-03-13 08:09:20 +00:00

118 lines
3.3 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services\Intune;
use Illuminate\Support\Str;
final class SecretClassificationService
{
public const string REDACTED = '[REDACTED]';
public const int REDACTION_VERSION = 1;
/**
* @var array<int, string>
*/
private const array PROTECTED_FIELD_NAMES = [
'access_token',
'apikey',
'api_key',
'authorization',
'bearer',
'bearer_token',
'client_secret',
'cookie',
'password',
'presharedkey',
'pre_shared_key',
'private_key',
'refresh_token',
'sas_token',
'secret',
'set-cookie',
'shared_secret',
'token',
];
/**
* @var array<string, array<int, string>>
*/
private const array PROTECTED_JSON_POINTERS = [
'snapshot' => [
'/wifi/password',
'/authentication/clientSecret',
],
'assignments' => [],
'scope_tags' => [],
'audit' => [],
'verification' => [],
'ops_failure' => [],
];
public function protectsField(string $sourceBucket, string $fieldName, ?string $jsonPointer = null): bool
{
$fieldName = $this->normalizeFieldName($fieldName);
if ($fieldName === '') {
return false;
}
$protectedPointers = self::PROTECTED_JSON_POINTERS[$sourceBucket] ?? [];
if (is_string($jsonPointer) && in_array($jsonPointer, $protectedPointers, true)) {
return true;
}
return in_array($fieldName, self::PROTECTED_FIELD_NAMES, true);
}
public function sanitizeAuditString(string $value): string
{
return $this->sanitizeMessageLikeString($value, '[REDACTED]');
}
public function sanitizeOpsFailureString(string $value): string
{
$sanitized = $this->sanitizeMessageLikeString($value, '[REDACTED_SECRET]');
$sanitized = preg_replace('/(?:\[[A-Z_]+\]|[A-Z0-9._%+\-]+)@[A-Z0-9.\-]+\.[A-Z]{2,}/i', '[REDACTED_EMAIL]', $sanitized) ?? $sanitized;
return $sanitized;
}
private function sanitizeMessageLikeString(string $value, string $replacement): string
{
$patterns = [
'/\bAuthorization\s*:\s*Bearer\s+[A-Za-z0-9\-\._~\+\/]+=*/i',
'/\bBearer\s+[A-Za-z0-9\-\._~\+\/]+=*/i',
'/\b[A-Za-z0-9\-_]{20,}\.[A-Za-z0-9\-_]{20,}\.[A-Za-z0-9\-_]{20,}\b/',
];
foreach ($patterns as $pattern) {
$value = preg_replace($pattern, $replacement, $value) ?? $value;
}
foreach (self::PROTECTED_FIELD_NAMES as $fieldName) {
$quotedPattern = sprintf('/"%s"\s*:\s*"[^"]*"/i', preg_quote($fieldName, '/'));
$pairPattern = sprintf('/\b%s\b\s*[:=]\s*[^\s,;]+/i', preg_quote($fieldName, '/'));
$value = preg_replace($quotedPattern, sprintf('"%s":"%s"', $fieldName, $replacement), $value) ?? $value;
$value = preg_replace($pairPattern, sprintf('%s=%s', $fieldName, $replacement), $value) ?? $value;
}
return $value;
}
private function normalizeFieldName(string $fieldName): string
{
$fieldName = Str::of($fieldName)
->replace(['-', ' '], '_')
->snake()
->lower()
->toString();
return trim($fieldName);
}
}