Hydrate configurationPolicies/{id}/settings for endpoint security/baseline policies so snapshots include real rule data.
Treat those types like Settings Catalog policies in the normalizer so they show the searchable settings table, recognizable categories, and readable choice values (firewall-specific formatting + interface badge parsing).
Improve “General” tab cards: badge lists for platforms/technologies, template reference summary (name/family/version/ID), and ISO timestamps rendered as YYYY‑MM‑DD HH:MM:SS; added regression test for the view.
Co-authored-by: Ahmed Darrazi <ahmeddarrazi@adsmac.local>
Reviewed-on: #23
339 lines
16 KiB
PHP
339 lines
16 KiB
PHP
@php
|
|
use Illuminate\Support\Str;
|
|
|
|
// Extract state from Filament ViewEntry
|
|
$state = $getState();
|
|
$status = $state['status'] ?? 'success';
|
|
$warnings = $state['warnings'] ?? [];
|
|
$settings = $state['settings'] ?? [];
|
|
$settingsTable = $state['settings_table'] ?? null;
|
|
|
|
$policyType = $state['policy_type'] ?? null;
|
|
|
|
$stringifyValue = function (mixed $value): string {
|
|
if (is_null($value)) {
|
|
return 'N/A';
|
|
}
|
|
|
|
if (is_bool($value)) {
|
|
return $value ? 'Enabled' : 'Disabled';
|
|
}
|
|
|
|
if (is_scalar($value)) {
|
|
return (string) $value;
|
|
}
|
|
|
|
if (is_array($value)) {
|
|
$encoded = json_encode($value, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
|
|
|
return is_string($encoded) ? $encoded : 'N/A';
|
|
}
|
|
|
|
if (is_object($value)) {
|
|
if (method_exists($value, '__toString')) {
|
|
return (string) $value;
|
|
}
|
|
|
|
$encoded = json_encode((array) $value, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
|
|
|
return is_string($encoded) ? $encoded : 'N/A';
|
|
}
|
|
|
|
return 'N/A';
|
|
};
|
|
|
|
$shouldRenderBadges = function (mixed $value): bool {
|
|
if (! is_array($value) || $value === []) {
|
|
return false;
|
|
}
|
|
|
|
if (! array_is_list($value)) {
|
|
return false;
|
|
}
|
|
|
|
foreach ($value as $item) {
|
|
if (! is_scalar($item) && ! is_null($item)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
$asEnabledDisabledBadgeValue = function (mixed $value): ?bool {
|
|
if (is_bool($value)) {
|
|
return $value;
|
|
}
|
|
|
|
if (! is_string($value)) {
|
|
return null;
|
|
}
|
|
|
|
$normalized = strtolower(trim($value));
|
|
|
|
return match ($normalized) {
|
|
'enabled', 'true', 'yes', '1' => true,
|
|
'disabled', 'false', 'no', '0' => false,
|
|
default => null,
|
|
};
|
|
};
|
|
@endphp
|
|
|
|
<div class="space-y-4">
|
|
{{-- Warnings --}}
|
|
@if(!empty($warnings))
|
|
<x-filament::section>
|
|
<div class="space-y-2">
|
|
@foreach($warnings as $warning)
|
|
<div class="flex items-start gap-2 text-sm text-warning-600 dark:text-warning-400">
|
|
<svg class="w-5 h-5 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
|
|
</svg>
|
|
<span>{{ $warning }}</span>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
</x-filament::section>
|
|
@endif
|
|
|
|
{{-- Settings Table (for Settings Catalog legacy format) --}}
|
|
@if($settingsTable && !empty($settingsTable['rows']))
|
|
<x-filament::section
|
|
:heading="$settingsTable['title'] ?? 'Settings'"
|
|
:description="$settingsTable['description'] ?? null"
|
|
>
|
|
<x-slot name="headerEnd">
|
|
<span class="text-sm text-gray-500 dark:text-gray-400">
|
|
{{ count($settingsTable['rows']) }} {{ Str::plural('setting', count($settingsTable['rows'])) }}
|
|
</span>
|
|
</x-slot>
|
|
|
|
<div class="divide-y divide-gray-200 dark:divide-gray-700">
|
|
@foreach($settingsTable['rows'] as $row)
|
|
<div class="py-3 sm:grid sm:grid-cols-3 sm:gap-4">
|
|
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">
|
|
{{ $row['definition'] ?? $row['label'] ?? $row['path'] ?? 'Setting' }}
|
|
</dt>
|
|
<dd class="mt-1 sm:mt-0 sm:col-span-2">
|
|
<span class="text-sm text-gray-900 dark:text-white">
|
|
@php
|
|
$badgeValue = $asEnabledDisabledBadgeValue($row['value'] ?? null);
|
|
@endphp
|
|
|
|
@if(! is_null($badgeValue))
|
|
<x-filament::badge :color="$badgeValue ? 'success' : 'gray'" size="sm">
|
|
{{ $badgeValue ? 'Enabled' : 'Disabled' }}
|
|
</x-filament::badge>
|
|
@elseif(is_numeric($row['value']))
|
|
<span class="font-mono font-semibold">{{ $row['value'] }}</span>
|
|
@else
|
|
{{ $row['value'] ?? 'N/A' }}
|
|
@endif
|
|
</span>
|
|
</dd>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
</x-filament::section>
|
|
@endif
|
|
|
|
{{-- Settings Blocks (for OMA Settings, Key/Value pairs, etc.) --}}
|
|
@foreach($settings as $block)
|
|
@php
|
|
$blockType = is_array($block) ? ($block['type'] ?? null) : null;
|
|
@endphp
|
|
|
|
@if($blockType === 'table')
|
|
<x-filament::section
|
|
:heading="$block['title'] ?? 'Settings'"
|
|
collapsible
|
|
>
|
|
<x-slot name="headerEnd">
|
|
<span class="text-sm text-gray-500 dark:text-gray-400">
|
|
{{ count($block['rows'] ?? []) }} {{ Str::plural('item', count($block['rows'] ?? [])) }}
|
|
</span>
|
|
</x-slot>
|
|
|
|
<div class="divide-y divide-gray-200 dark:divide-gray-700">
|
|
@foreach($block['rows'] ?? [] as $row)
|
|
<div class="py-3 sm:grid sm:grid-cols-3 sm:gap-4">
|
|
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400 break-words">
|
|
{{ $row['label'] ?? $row['path'] ?? 'Setting' }}
|
|
@if(!empty($row['description']))
|
|
<p class="text-xs text-gray-400 mt-0.5">{{ Str::limit($row['description'], 80) }}</p>
|
|
@endif
|
|
</dt>
|
|
<dd class="mt-1 sm:mt-0 sm:col-span-2">
|
|
@php
|
|
$badgeValue = $asEnabledDisabledBadgeValue($row['value'] ?? null);
|
|
@endphp
|
|
|
|
@if(! is_null($badgeValue))
|
|
<x-filament::badge :color="$badgeValue ? 'success' : 'gray'" size="sm">
|
|
{{ $badgeValue ? 'Enabled' : 'Disabled' }}
|
|
</x-filament::badge>
|
|
@elseif(is_numeric($row['value']))
|
|
<span class="font-mono text-sm font-semibold text-gray-900 dark:text-white">
|
|
{{ $row['value'] }}
|
|
</span>
|
|
@elseif($shouldRenderBadges($row['value'] ?? null))
|
|
<div class="flex flex-wrap gap-1.5">
|
|
@foreach(($row['value'] ?? []) as $item)
|
|
<x-filament::badge color="gray" size="sm">
|
|
{{ is_bool($item) ? ($item ? 'Enabled' : 'Disabled') : (string) $item }}
|
|
</x-filament::badge>
|
|
@endforeach
|
|
</div>
|
|
@else
|
|
<span class="text-sm text-gray-900 dark:text-white break-words">
|
|
{{ Str::limit($stringifyValue($row['value'] ?? null), 200) }}
|
|
</span>
|
|
@endif
|
|
</dd>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
</x-filament::section>
|
|
|
|
@elseif($blockType === 'keyValue')
|
|
<x-filament::section
|
|
:heading="$block['title'] ?? 'Settings'"
|
|
collapsible
|
|
>
|
|
<x-slot name="headerEnd">
|
|
<span class="text-sm text-gray-500 dark:text-gray-400">
|
|
{{ count($block['entries'] ?? []) }} {{ Str::plural('entry', count($block['entries'] ?? [])) }}
|
|
</span>
|
|
</x-slot>
|
|
|
|
<div class="divide-y divide-gray-200 dark:divide-gray-700">
|
|
@foreach($block['entries'] ?? [] as $entry)
|
|
<div class="py-3 sm:grid sm:grid-cols-3 sm:gap-4">
|
|
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">
|
|
{{ $entry['key'] }}
|
|
</dt>
|
|
<dd class="mt-1 sm:mt-0 sm:col-span-2">
|
|
@php
|
|
$rawValue = $entry['value'] ?? null;
|
|
|
|
$isScriptContent = in_array($entry['key'] ?? null, ['scriptContent', 'detectionScriptContent', 'remediationScriptContent'], true)
|
|
&& (bool) config('tenantpilot.display.show_script_content', false);
|
|
|
|
$badgeValue = $asEnabledDisabledBadgeValue($rawValue);
|
|
@endphp
|
|
|
|
@if($isScriptContent)
|
|
@php
|
|
$code = is_string($rawValue) ? $rawValue : $stringifyValue($rawValue);
|
|
$firstLine = strtok($code, "\n") ?: '';
|
|
|
|
$grammar = 'powershell';
|
|
|
|
if ($policyType === 'deviceShellScript') {
|
|
$shebang = trim($firstLine);
|
|
|
|
if (str_starts_with($shebang, '#!')) {
|
|
if (str_contains($shebang, 'zsh')) {
|
|
$grammar = 'zsh';
|
|
} elseif (str_contains($shebang, 'bash')) {
|
|
$grammar = 'bash';
|
|
} else {
|
|
$grammar = 'sh';
|
|
}
|
|
} else {
|
|
$grammar = 'sh';
|
|
}
|
|
} elseif ($policyType === 'deviceManagementScript' || $policyType === 'deviceHealthScript') {
|
|
$grammar = 'powershell';
|
|
}
|
|
|
|
$highlightedHtml = null;
|
|
|
|
if (class_exists(\Torchlight\Engine\Engine::class)) {
|
|
try {
|
|
$highlightedHtml = (new \Torchlight\Engine\Engine())->codeToHtml(
|
|
code: $code,
|
|
grammar: $grammar,
|
|
theme: [
|
|
'light' => 'github-light',
|
|
'dark' => 'github-dark',
|
|
],
|
|
withGutter: false,
|
|
withWrapper: true,
|
|
);
|
|
} catch (\Throwable $e) {
|
|
$highlightedHtml = null;
|
|
}
|
|
}
|
|
@endphp
|
|
|
|
<div x-data="{ open: false }" class="space-y-2">
|
|
<div class="flex items-center gap-2">
|
|
<x-filament::button
|
|
size="xs"
|
|
color="gray"
|
|
type="button"
|
|
x-on:click="open = !open"
|
|
>
|
|
<span x-show="!open" x-cloak>Show</span>
|
|
<span x-show="open" x-cloak>Hide</span>
|
|
</x-filament::button>
|
|
|
|
<span class="text-xs text-gray-500 dark:text-gray-400">
|
|
{{ number_format(Str::length($code)) }} chars
|
|
</span>
|
|
</div>
|
|
|
|
<div x-show="open" x-cloak>
|
|
@if (is_string($highlightedHtml) && $highlightedHtml !== '')
|
|
@once
|
|
@include('filament.partials.torchlight-dark-overrides')
|
|
@endonce
|
|
|
|
<div class="overflow-x-auto">{!! $highlightedHtml !!}</div>
|
|
@else
|
|
<pre class="text-xs font-mono text-gray-900 dark:text-white whitespace-pre-wrap break-words">{{ $code }}</pre>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
@elseif($shouldRenderBadges($rawValue))
|
|
<div class="flex flex-wrap gap-1.5">
|
|
@foreach(($rawValue ?? []) as $item)
|
|
<x-filament::badge color="gray" size="sm">
|
|
{{ is_bool($item) ? ($item ? 'Enabled' : 'Disabled') : (string) $item }}
|
|
</x-filament::badge>
|
|
@endforeach
|
|
</div>
|
|
@elseif(! is_null($badgeValue))
|
|
<x-filament::badge :color="$badgeValue ? 'success' : 'gray'" size="sm">
|
|
{{ $badgeValue ? 'Enabled' : 'Disabled' }}
|
|
</x-filament::badge>
|
|
@else
|
|
<span class="text-sm text-gray-900 dark:text-white break-words">
|
|
{{ Str::limit($stringifyValue($rawValue), 200) }}
|
|
</span>
|
|
@endif
|
|
</dd>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
</x-filament::section>
|
|
@endif
|
|
@endforeach
|
|
|
|
{{-- Empty state --}}
|
|
@if(empty($settings) && (!$settingsTable || empty($settingsTable['rows'])))
|
|
<div class="text-center py-12">
|
|
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
|
</svg>
|
|
<p class="mt-4 text-sm text-gray-500 dark:text-gray-400">
|
|
No settings data available
|
|
</p>
|
|
<p class="mt-1 text-xs text-gray-400 dark:text-gray-500">
|
|
This policy may not contain settings, or they are in an unsupported format
|
|
</p>
|
|
</div>
|
|
@endif
|
|
</div>
|