## 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
118 lines
3.3 KiB
PHP
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);
|
|
}
|
|
}
|