TenantAtlas/app/Services/Intune/PolicySnapshotRedactor.php
2026-03-07 17:41:55 +01:00

148 lines
5.2 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services\Intune;
final class PolicySnapshotRedactor
{
public function __construct(
private readonly ?SecretClassificationService $classificationService = null,
private readonly ?SecretFingerprintHasher $fingerprintHasher = null,
) {}
/**
* @param array<string, mixed> $payload
* @return array<string, mixed>
*/
public function redactPayload(array $payload): array
{
$redacted = $this->protectBucket('snapshot', $payload, null);
return is_array($redacted['value']) ? $redacted['value'] : $payload;
}
/**
* @param array<int, array<string, mixed>>|null $assignments
* @return array<int, array<string, mixed>>|null
*/
public function redactAssignments(?array $assignments): ?array
{
if ($assignments === null) {
return null;
}
$redacted = $this->protectBucket('assignments', $assignments, null);
return is_array($redacted['value']) ? $redacted['value'] : $assignments;
}
/**
* @param array<int, array<string, mixed>>|null $scopeTags
* @return array<int, array<string, mixed>>|null
*/
public function redactScopeTags(?array $scopeTags): ?array
{
if ($scopeTags === null) {
return null;
}
$redacted = $this->protectBucket('scope_tags', $scopeTags, null);
return is_array($redacted['value']) ? $redacted['value'] : $scopeTags;
}
/**
* @param array<string, mixed> $payload
* @param array<int, array<string, mixed>>|null $assignments
* @param array<int, array<string, mixed>>|array<string, mixed>|null $scopeTags
*/
public function protect(int $workspaceId, array $payload, ?array $assignments = null, ?array $scopeTags = null): ProtectedSnapshotResult
{
$snapshot = $this->protectBucket('snapshot', $payload, $workspaceId);
$protectedAssignments = $assignments !== null ? $this->protectBucket('assignments', $assignments, $workspaceId) : null;
$protectedScopeTags = $scopeTags !== null ? $this->protectBucket('scope_tags', $scopeTags, $workspaceId) : null;
$secretFingerprints = ProtectedSnapshotResult::emptyFingerprints();
$secretFingerprints['snapshot'] = $snapshot['fingerprints'];
$secretFingerprints['assignments'] = $protectedAssignments['fingerprints'] ?? [];
$secretFingerprints['scope_tags'] = $protectedScopeTags['fingerprints'] ?? [];
return new ProtectedSnapshotResult(
snapshot: is_array($snapshot['value']) ? $snapshot['value'] : $payload,
assignments: $protectedAssignments !== null && is_array($protectedAssignments['value']) ? $protectedAssignments['value'] : $assignments,
scopeTags: $protectedScopeTags !== null && is_array($protectedScopeTags['value']) ? $protectedScopeTags['value'] : $scopeTags,
secretFingerprints: $secretFingerprints,
redactionVersion: SecretClassificationService::REDACTION_VERSION,
protectedPathsCount: count($secretFingerprints['snapshot']) + count($secretFingerprints['assignments']) + count($secretFingerprints['scope_tags']),
);
}
/**
* @param array<int, string> $segments
* @return array{value: mixed, fingerprints: array<string, string>}
*/
private function protectBucket(string $sourceBucket, mixed $value, ?int $workspaceId, array $segments = []): array
{
if (! is_array($value)) {
return [
'value' => $value,
'fingerprints' => [],
];
}
$redacted = [];
$fingerprints = [];
foreach ($value as $key => $item) {
$nextSegments = [...$segments, (string) $key];
$jsonPointer = $this->jsonPointer($nextSegments);
if (is_string($key) && $this->classifier()->protectsField($sourceBucket, $key, $jsonPointer)) {
$redacted[$key] = SecretClassificationService::REDACTED;
if ($workspaceId !== null) {
$fingerprints[$jsonPointer] = $this->hasher()->fingerprint($workspaceId, $sourceBucket, $jsonPointer, $item);
}
continue;
}
$protected = $this->protectBucket($sourceBucket, $item, $workspaceId, $nextSegments);
$redacted[$key] = $protected['value'];
$fingerprints = [...$fingerprints, ...$protected['fingerprints']];
}
return [
'value' => $redacted,
'fingerprints' => $fingerprints,
];
}
/**
* @param array<int, string> $segments
*/
private function jsonPointer(array $segments): string
{
if ($segments === []) {
return '/';
}
return '/'.implode('/', array_map(
static fn (string $segment): string => str_replace(['~', '/'], ['~0', '~1'], $segment),
$segments,
));
}
private function classifier(): SecretClassificationService
{
return $this->classificationService ?? app(SecretClassificationService::class);
}
private function hasher(): SecretFingerprintHasher
{
return $this->fingerprintHasher ?? app(SecretFingerprintHasher::class);
}
}