## Summary - replace broad substring-based masking with a shared exact/path-based secret classifier and workspace-scoped fingerprint hashing - persist protected snapshot metadata on `policy_versions` and keep secret-only changes visible in compare, drift, restore, review, verification, and ops surfaces - add Spec 120 artifacts, audit documentation, and focused Pest regression coverage for snapshot, audit, verification, review-pack, and notification behavior ## Validation - `vendor/bin/sail artisan test --compact tests/Feature/Intune/PolicySnapshotRedactionTest.php tests/Feature/Intune/PolicySnapshotFingerprintIsolationTest.php tests/Feature/ReviewPack/ReviewPackRedactionIntegrityTest.php tests/Feature/OpsUx/OperationRunNotificationRedactionTest.php tests/Feature/Verification/VerificationReportViewerDbOnlyTest.php` - `vendor/bin/sail bin pint --dirty --format agent` ## Spec / checklist status | Checklist | Total | Completed | Incomplete | Status | |-----------|-------|-----------|------------|--------| | requirements.md | 16 | 16 | 0 | ✓ PASS | - `tasks.md`: T001-T032 complete - `tasks.md`: T033 manual quickstart validation is still open and noted for follow-up ## Filament / platform notes - Livewire v4 compliance is unchanged - no panel provider changes; `bootstrap/providers.php` remains the registration location - no new globally searchable resources were introduced, so global search requirements are unchanged - no new destructive Filament actions were added - no new Filament assets were added; no `filament:assets` deployment change is required ## Testing coverage touched - snapshot persistence and fingerprint isolation - compare/drift protected-change evidence - audit, verification, review-pack, ops-failure, and notification sanitization - viewer/read-only Filament presentation updates Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #146
97 lines
2.2 KiB
PHP
97 lines
2.2 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Support\OpsUx;
|
|
|
|
final class SummaryCountsNormalizer
|
|
{
|
|
/**
|
|
* @param array<string, mixed> $summaryCounts
|
|
* @return array<string, int>
|
|
*/
|
|
public static function normalize(array $summaryCounts): array
|
|
{
|
|
$allowedKeys = array_flip(OperationSummaryKeys::all());
|
|
|
|
$sanitized = [];
|
|
|
|
foreach ($summaryCounts as $key => $value) {
|
|
$key = trim((string) $key);
|
|
|
|
if ($key === '' || ! isset($allowedKeys[$key])) {
|
|
continue;
|
|
}
|
|
|
|
if (is_int($value)) {
|
|
$sanitized[$key] = $value;
|
|
|
|
continue;
|
|
}
|
|
|
|
if (is_float($value) && is_finite($value)) {
|
|
$sanitized[$key] = (int) round($value);
|
|
|
|
continue;
|
|
}
|
|
|
|
if (is_numeric($value)) {
|
|
$sanitized[$key] = (int) $value;
|
|
}
|
|
}
|
|
|
|
$ordered = [];
|
|
|
|
foreach (OperationSummaryKeys::all() as $key) {
|
|
if (array_key_exists($key, $sanitized)) {
|
|
$ordered[$key] = $sanitized[$key];
|
|
}
|
|
}
|
|
|
|
return $ordered;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $summaryCounts
|
|
*/
|
|
public static function renderSummaryLine(array $summaryCounts): ?string
|
|
{
|
|
$normalized = self::normalize($summaryCounts);
|
|
|
|
if ($normalized === []) {
|
|
return null;
|
|
}
|
|
|
|
$parts = [];
|
|
|
|
foreach ($normalized as $key => $value) {
|
|
if ($value === 0) {
|
|
continue;
|
|
}
|
|
|
|
$parts[] = self::humanizeKey($key).': '.$value;
|
|
}
|
|
|
|
if ($parts === []) {
|
|
return null;
|
|
}
|
|
|
|
return implode(' · ', $parts);
|
|
}
|
|
|
|
/**
|
|
* Convert a snake_case summary key to a human-readable label.
|
|
*/
|
|
private static function humanizeKey(string $key): string
|
|
{
|
|
return match ($key) {
|
|
'items' => 'Affected items',
|
|
'tenants' => 'Tenants',
|
|
'finding_count' => 'Findings',
|
|
'report_count' => 'Reports',
|
|
'operation_count' => 'Operations',
|
|
default => ucfirst(str_replace('_', ' ', $key)),
|
|
};
|
|
}
|
|
}
|