TenantAtlas/apps/platform/app/Services/Intune/SecretClassificationService.php
ahmido ce0615a9c1 Spec 182: relocate Laravel platform to apps/platform (#213)
## Summary
- move the Laravel application into `apps/platform` and keep the repository root for orchestration, docs, and tooling
- update the local command model, Sail/Docker wiring, runtime paths, and ignore rules around the new platform location
- add relocation quickstart/contracts plus focused smoke coverage for bootstrap, command model, routes, and runtime behavior

## Validation
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/PlatformRelocation`
- integrated browser smoke validated `/up`, `/`, `/admin`, `/admin/choose-workspace`, and tenant route semantics for `200`, `403`, and `404`

## Remaining Rollout Checks
- validate Dokploy build context and working-directory assumptions against the new `apps/platform` layout
- confirm web, queue, and scheduler processes all start from the expected working directory in staging/production
- verify no legacy volume mounts or asset-publish paths still point at the old root-level `public/` or `storage/` locations

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #213
2026-04-08 08:40:47 +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);
}
}