TenantAtlas/apps/platform/app/Support/RedactionIntegrity.php
Ahmed Darrazi c125fd48fd
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 3m58s
feat(ui): implement diagnostic entry point consolidation
Applied diagnostic surface contract rules to Audit Log inspect modal and Support Diagnostics action context, consolidating raw diagnostic data into safe modals according to Spec 374.
2026-06-13 03:06:33 +02:00

175 lines
4.6 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Support;
use App\Models\OperationRun;
use App\Models\PolicyVersion;
use App\Services\Intune\SecretClassificationService;
final class RedactionIntegrity
{
public static function protectedValueNote(): string
{
return 'Protected values are intentionally hidden as [REDACTED]. Secret-only changes remain detectable without revealing the value.';
}
public static function supportDiagnosticsNote(): string
{
return 'Support diagnostics use a redacted support view. Secrets, credentials, raw provider payloads, full response bodies, and unrestricted log excerpts are intentionally excluded.';
}
/**
* @return list<array{path: ?string, reason: string, replacement_text: string}>
*/
public static function supportDiagnosticsMarkers(): array
{
return [
[
'path' => 'provider_connection.credential',
'reason' => 'credential',
'replacement_text' => '[REDACTED]',
],
[
'path' => 'stored_reports.payload',
'reason' => 'raw_payload',
'replacement_text' => '[REDACTED]',
],
[
'path' => 'audit_logs.metadata.raw',
'reason' => 'restricted_log_excerpt',
'replacement_text' => '[REDACTED]',
],
];
}
public static function noteForPolicyVersion(PolicyVersion $version): ?string
{
if (self::fingerprintCount($version->secret_fingerprints) > 0) {
return self::protectedValueNote();
}
return null;
}
/**
* @param array<string, mixed> $evidence
*/
public static function noteForFindingEvidence(array $evidence): ?string
{
$notes = self::findingEvidenceNotes($evidence);
return $notes === [] ? null : implode(' ', $notes);
}
public static function noteForRun(OperationRun $run): ?string
{
$context = is_array($run->context) ? $run->context : [];
$integrityNote = data_get($context, 'redaction_integrity.note');
if (is_string($integrityNote) && trim($integrityNote) !== '') {
return trim($integrityNote);
}
if ((bool) data_get($context, 'redaction_integrity.protected_values_hidden', false)) {
return self::protectedValueNote();
}
return null;
}
/**
* @param array<string, mixed>|null $report
* @return array<int, string>
*/
public static function verificationNotes(?array $report): array
{
if (! is_array($report)) {
return [];
}
if (! self::containsPlaceholderFragment($report)) {
return [];
}
return [self::protectedValueNote()];
}
/**
* @param array<string, mixed>|null $fingerprints
*/
public static function fingerprintCount(?array $fingerprints): int
{
if (! is_array($fingerprints)) {
return 0;
}
$count = 0;
foreach ($fingerprints as $bucket) {
if (! is_array($bucket)) {
continue;
}
foreach ($bucket as $digest) {
if (is_string($digest) && trim($digest) !== '') {
$count++;
}
}
}
return $count;
}
/**
* @param array<string, mixed> $evidence
*/
private static function evidenceHasProtectedData(array $evidence): bool
{
foreach (['baseline', 'current'] as $side) {
$fingerprints = data_get($evidence, "{$side}.secret_fingerprints");
if (is_array($fingerprints) && self::fingerprintCount($fingerprints) > 0) {
return true;
}
}
return false;
}
/**
* @param array<string, mixed> $evidence
* @return array<int, string>
*/
private static function findingEvidenceNotes(array $evidence): array
{
$notes = [];
if (self::evidenceHasProtectedData($evidence)) {
$notes[] = self::protectedValueNote();
}
return array_values(array_unique($notes));
}
private static function containsPlaceholderFragment(mixed $value): bool
{
if (is_string($value)) {
return str_contains($value, SecretClassificationService::REDACTED);
}
if (! is_array($value)) {
return false;
}
foreach ($value as $item) {
if (self::containsPlaceholderFragment($item)) {
return true;
}
}
return false;
}
}