$payload * @return array */ public function redactPayload(array $payload): array { $redacted = $this->protectBucket('snapshot', $payload, null); return is_array($redacted['value']) ? $redacted['value'] : $payload; } /** * @param array>|null $assignments * @return array>|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>|null $scopeTags * @return array>|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 $payload * @param array>|null $assignments * @param array>|array|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 $segments * @return array{value: mixed, fingerprints: array} */ 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 $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); } }