feat: scripts normalized settings and safe script content view
This commit is contained in:
parent
058724c359
commit
17bfc2f17e
@ -623,6 +623,7 @@ private static function normalizedPolicyState(Policy $record): array
|
|||||||
|
|
||||||
$normalized['context'] = 'policy';
|
$normalized['context'] = 'policy';
|
||||||
$normalized['record_id'] = (string) $record->getKey();
|
$normalized['record_id'] = (string) $record->getKey();
|
||||||
|
$normalized['policy_type'] = $record->policy_type;
|
||||||
|
|
||||||
$request->attributes->set($cacheKey, $normalized);
|
$request->attributes->set($cacheKey, $normalized);
|
||||||
|
|
||||||
|
|||||||
@ -87,6 +87,7 @@ public static function infolist(Schema $schema): Schema
|
|||||||
|
|
||||||
$normalized['context'] = 'version';
|
$normalized['context'] = 'version';
|
||||||
$normalized['record_id'] = (string) $record->getKey();
|
$normalized['record_id'] = (string) $record->getKey();
|
||||||
|
$normalized['policy_type'] = $record->policy_type;
|
||||||
|
|
||||||
return $normalized;
|
return $normalized;
|
||||||
})
|
})
|
||||||
|
|||||||
@ -39,22 +39,7 @@ public function normalize(?array $snapshot, string $policyType, ?string $platfor
|
|||||||
$entries[] = ['key' => 'Description', 'value' => $description];
|
$entries[] = ['key' => 'Description', 'value' => $description];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Script content and large blobs should not dominate normalized output.
|
$entries = array_merge($entries, $this->contentEntries($snapshot));
|
||||||
// Keep only safe summary fields if present.
|
|
||||||
$contentKeys = [
|
|
||||||
'scriptContent',
|
|
||||||
'scriptContentBase64',
|
|
||||||
'detectionScriptContent',
|
|
||||||
'remediationScriptContent',
|
|
||||||
];
|
|
||||||
|
|
||||||
foreach ($contentKeys as $key) {
|
|
||||||
$value = Arr::get($snapshot, $key);
|
|
||||||
|
|
||||||
if (is_string($value) && $value !== '') {
|
|
||||||
$entries[] = ['key' => $key, 'value' => sprintf('[content: %d chars]', strlen($value))];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$schedule = Arr::get($snapshot, 'runSchedule');
|
$schedule = Arr::get($snapshot, 'runSchedule');
|
||||||
if (is_array($schedule) && $schedule !== []) {
|
if (is_array($schedule) && $schedule !== []) {
|
||||||
@ -84,6 +69,173 @@ public function normalize(?array $snapshot, string $policyType, ?string $platfor
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, array{key: string, value: mixed}>
|
||||||
|
*/
|
||||||
|
private function contentEntries(array $snapshot): array
|
||||||
|
{
|
||||||
|
$showContent = (bool) config('tenantpilot.display.show_script_content', false);
|
||||||
|
$maxChars = (int) config('tenantpilot.display.max_script_content_chars', 5000);
|
||||||
|
if ($maxChars <= 0) {
|
||||||
|
$maxChars = 5000;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $showContent) {
|
||||||
|
return $this->contentSummaryEntries($snapshot);
|
||||||
|
}
|
||||||
|
|
||||||
|
$entries = [];
|
||||||
|
|
||||||
|
$scriptContent = Arr::get($snapshot, 'scriptContent');
|
||||||
|
if (is_string($scriptContent) && $scriptContent !== '') {
|
||||||
|
$decoded = $this->decodeIfBase64Text($scriptContent);
|
||||||
|
if (is_string($decoded) && $decoded !== '') {
|
||||||
|
$scriptContent = $decoded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! is_string($scriptContent) || $scriptContent === '') {
|
||||||
|
$scriptContentBase64 = Arr::get($snapshot, 'scriptContentBase64');
|
||||||
|
if (is_string($scriptContentBase64) && $scriptContentBase64 !== '') {
|
||||||
|
$decoded = base64_decode($this->stripWhitespace($scriptContentBase64), true);
|
||||||
|
if (is_string($decoded) && $decoded !== '') {
|
||||||
|
$scriptContent = $this->normalizeDecodedText($decoded);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_string($scriptContent) && $scriptContent !== '') {
|
||||||
|
$entries[] = ['key' => 'scriptContent', 'value' => $this->limitContent($scriptContent, $maxChars)];
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (['detectionScriptContent', 'remediationScriptContent'] as $key) {
|
||||||
|
$value = Arr::get($snapshot, $key);
|
||||||
|
|
||||||
|
if (! is_string($value) || $value === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$decoded = $this->decodeIfBase64Text($value);
|
||||||
|
if (is_string($decoded) && $decoded !== '') {
|
||||||
|
$value = $decoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
$entries[] = ['key' => $key, 'value' => $this->limitContent($value, $maxChars)];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function decodeIfBase64Text(string $candidate): ?string
|
||||||
|
{
|
||||||
|
$trimmed = $this->stripWhitespace($candidate);
|
||||||
|
if ($trimmed === '' || strlen($trimmed) < 16) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strlen($trimmed) % 4 !== 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! preg_match('/^[A-Za-z0-9+\/=]+$/', $trimmed)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$decoded = base64_decode($trimmed, true);
|
||||||
|
if (! is_string($decoded) || $decoded === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$decoded = $this->normalizeDecodedText($decoded);
|
||||||
|
if ($decoded === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $this->looksLikeText($decoded)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $decoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function stripWhitespace(string $value): string
|
||||||
|
{
|
||||||
|
return preg_replace('/\s+/', '', $value) ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeDecodedText(string $decoded): string
|
||||||
|
{
|
||||||
|
if (str_starts_with($decoded, "\xFF\xFE")) {
|
||||||
|
$decoded = mb_convert_encoding(substr($decoded, 2), 'UTF-8', 'UTF-16LE');
|
||||||
|
} elseif (str_starts_with($decoded, "\xFE\xFF")) {
|
||||||
|
$decoded = mb_convert_encoding(substr($decoded, 2), 'UTF-8', 'UTF-16BE');
|
||||||
|
} elseif (str_contains($decoded, "\x00")) {
|
||||||
|
$decoded = mb_convert_encoding($decoded, 'UTF-8', 'UTF-16LE');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_starts_with($decoded, "\xEF\xBB\xBF")) {
|
||||||
|
$decoded = substr($decoded, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $decoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function looksLikeText(string $decoded): bool
|
||||||
|
{
|
||||||
|
$length = strlen($decoded);
|
||||||
|
if ($length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$nonPrintable = preg_match_all('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', $decoded) ?: 0;
|
||||||
|
if ($nonPrintable > (int) max(1, $length * 0.05)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scripts should typically contain some whitespace or line breaks.
|
||||||
|
if ($length >= 24 && ! preg_match('/\s/', $decoded)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, array{key: string, value: mixed}>
|
||||||
|
*/
|
||||||
|
private function contentSummaryEntries(array $snapshot): array
|
||||||
|
{
|
||||||
|
// Script content and large blobs should not dominate normalized output.
|
||||||
|
// Keep only safe summary fields if present.
|
||||||
|
$contentKeys = [
|
||||||
|
'scriptContent',
|
||||||
|
'scriptContentBase64',
|
||||||
|
'detectionScriptContent',
|
||||||
|
'remediationScriptContent',
|
||||||
|
];
|
||||||
|
|
||||||
|
$entries = [];
|
||||||
|
|
||||||
|
foreach ($contentKeys as $key) {
|
||||||
|
$value = Arr::get($snapshot, $key);
|
||||||
|
|
||||||
|
if (is_string($value) && $value !== '') {
|
||||||
|
$entries[] = ['key' => $key, 'value' => sprintf('[content: %d chars]', strlen($value))];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function limitContent(string $content, int $maxChars): string
|
||||||
|
{
|
||||||
|
if (mb_strlen($content) <= $maxChars) {
|
||||||
|
return $content;
|
||||||
|
}
|
||||||
|
|
||||||
|
return mb_substr($content, 0, $maxChars).'…';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array<string, mixed>
|
* @return array<string, mixed>
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -8,6 +8,7 @@
|
|||||||
"require": {
|
"require": {
|
||||||
"php": "^8.2",
|
"php": "^8.2",
|
||||||
"filament/filament": "^4.0",
|
"filament/filament": "^4.0",
|
||||||
|
"lara-zeus/torch-filament": "^2.0",
|
||||||
"laravel/framework": "^12.0",
|
"laravel/framework": "^12.0",
|
||||||
"laravel/tinker": "^2.10.1",
|
"laravel/tinker": "^2.10.1",
|
||||||
"pepperfm/filament-json": "^4"
|
"pepperfm/filament-json": "^4"
|
||||||
|
|||||||
193
composer.lock
generated
193
composer.lock
generated
@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "c4f08fd9fc4b86cc13b75332dd6e1b7a",
|
"content-hash": "20819254265bddd0aa70006919cb735f",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "anourvalar/eloquent-serialize",
|
"name": "anourvalar/eloquent-serialize",
|
||||||
@ -2082,6 +2082,87 @@
|
|||||||
},
|
},
|
||||||
"time": "2025-11-13T14:57:49+00:00"
|
"time": "2025-11-13T14:57:49+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "lara-zeus/torch-filament",
|
||||||
|
"version": "2.0.1",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/lara-zeus/torch-filament.git",
|
||||||
|
"reference": "71dbe8df4a558a80308781ba20c5922943b33009"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/lara-zeus/torch-filament/zipball/71dbe8df4a558a80308781ba20c5922943b33009",
|
||||||
|
"reference": "71dbe8df4a558a80308781ba20c5922943b33009",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"filament/filament": "^4.0",
|
||||||
|
"php": "^8.1",
|
||||||
|
"spatie/laravel-package-tools": "^1.16",
|
||||||
|
"torchlight/engine": "^0.1.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"larastan/larastan": "^2.0",
|
||||||
|
"laravel/pint": "^1.0",
|
||||||
|
"nunomaduro/collision": "^7.0",
|
||||||
|
"nunomaduro/phpinsights": "^2.8",
|
||||||
|
"orchestra/testbench": "^8.0",
|
||||||
|
"phpstan/extension-installer": "^1.1"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"laravel": {
|
||||||
|
"providers": [
|
||||||
|
"LaraZeus\\TorchFilament\\TorchFilamentServiceProvider"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"LaraZeus\\TorchFilament\\": "src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Lara Zeus",
|
||||||
|
"email": "info@larazeus.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Infolist component to highlight code using Torchlight Engine",
|
||||||
|
"homepage": "https://larazeus.com/torch-filament",
|
||||||
|
"keywords": [
|
||||||
|
"code",
|
||||||
|
"design",
|
||||||
|
"engine",
|
||||||
|
"filamentphp",
|
||||||
|
"highlight",
|
||||||
|
"input",
|
||||||
|
"lara-zeus",
|
||||||
|
"laravel",
|
||||||
|
"torchlight",
|
||||||
|
"ui"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/lara-zeus/torch-filament/issues",
|
||||||
|
"source": "https://github.com/lara-zeus/torch-filament"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://www.buymeacoffee.com/larazeus",
|
||||||
|
"type": "custom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/atmonshi",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2025-06-11T19:32:10+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "laravel/framework",
|
"name": "laravel/framework",
|
||||||
"version": "v12.42.0",
|
"version": "v12.42.0",
|
||||||
@ -4265,6 +4346,60 @@
|
|||||||
},
|
},
|
||||||
"time": "2025-02-26T00:08:40+00:00"
|
"time": "2025-02-26T00:08:40+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "phiki/phiki",
|
||||||
|
"version": "v1.1.6",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/phikiphp/phiki.git",
|
||||||
|
"reference": "3174d8cb309bdccc32b7a33500379de76148256b"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/phikiphp/phiki/zipball/3174d8cb309bdccc32b7a33500379de76148256b",
|
||||||
|
"reference": "3174d8cb309bdccc32b7a33500379de76148256b",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"league/commonmark": "^2.5.3",
|
||||||
|
"php": "^8.2"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"illuminate/support": "^11.30",
|
||||||
|
"laravel/pint": "^1.18.1",
|
||||||
|
"pestphp/pest": "^3.5.1",
|
||||||
|
"phpstan/extension-installer": "^1.4.3",
|
||||||
|
"phpstan/phpstan": "^2.0",
|
||||||
|
"symfony/var-dumper": "^7.1.6"
|
||||||
|
},
|
||||||
|
"bin": [
|
||||||
|
"bin/phiki"
|
||||||
|
],
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Phiki\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Ryan Chandler",
|
||||||
|
"email": "support@ryangjchandler.co.uk",
|
||||||
|
"homepage": "https://ryangjchandler.co.uk",
|
||||||
|
"role": "Developer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Syntax highlighting using TextMate grammars in PHP.",
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/phikiphp/phiki/issues",
|
||||||
|
"source": "https://github.com/phikiphp/phiki/tree/v1.1.6"
|
||||||
|
},
|
||||||
|
"time": "2025-06-06T20:18:29+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "phpoption/phpoption",
|
"name": "phpoption/phpoption",
|
||||||
"version": "1.9.4",
|
"version": "1.9.4",
|
||||||
@ -8110,6 +8245,62 @@
|
|||||||
},
|
},
|
||||||
"time": "2024-12-21T16:25:41+00:00"
|
"time": "2024-12-21T16:25:41+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "torchlight/engine",
|
||||||
|
"version": "v0.1.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/torchlight-api/engine.git",
|
||||||
|
"reference": "8d12f611efb0b22406ec0744abb453ddd2f1fe9d"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/torchlight-api/engine/zipball/8d12f611efb0b22406ec0744abb453ddd2f1fe9d",
|
||||||
|
"reference": "8d12f611efb0b22406ec0744abb453ddd2f1fe9d",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"league/commonmark": "^2.5.3",
|
||||||
|
"phiki/phiki": "^1.1.4",
|
||||||
|
"php": "^8.2"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"ext-dom": "*",
|
||||||
|
"ext-libxml": "*",
|
||||||
|
"laravel/pint": "^1.13",
|
||||||
|
"pestphp/pest": "^2"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Torchlight\\Engine\\": "src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Aaron Francis",
|
||||||
|
"email": "aaron@hammerstone.dev"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "John Koster",
|
||||||
|
"email": "john@stillat.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "The PHP-based Torchlight code annotation and rendering engine.",
|
||||||
|
"keywords": [
|
||||||
|
"Code highlighting",
|
||||||
|
"syntax highlighting"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/torchlight-api/engine/issues",
|
||||||
|
"source": "https://github.com/torchlight-api/engine/tree/v0.1.0"
|
||||||
|
},
|
||||||
|
"time": "2025-04-02T01:47:48+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "ueberdosis/tiptap-php",
|
"name": "ueberdosis/tiptap-php",
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
|
|||||||
@ -218,4 +218,9 @@
|
|||||||
'chunk_size' => (int) env('TENANTPILOT_BULK_CHUNK_SIZE', 10),
|
'chunk_size' => (int) env('TENANTPILOT_BULK_CHUNK_SIZE', 10),
|
||||||
'poll_interval_seconds' => (int) env('TENANTPILOT_BULK_POLL_INTERVAL_SECONDS', 3),
|
'poll_interval_seconds' => (int) env('TENANTPILOT_BULK_POLL_INTERVAL_SECONDS', 3),
|
||||||
],
|
],
|
||||||
|
|
||||||
|
'display' => [
|
||||||
|
'show_script_content' => (bool) env('TENANTPILOT_SHOW_SCRIPT_CONTENT', false),
|
||||||
|
'max_script_content_chars' => (int) env('TENANTPILOT_MAX_SCRIPT_CONTENT_CHARS', 5000),
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
|||||||
@ -7,6 +7,7 @@
|
|||||||
$warnings = $state['warnings'] ?? [];
|
$warnings = $state['warnings'] ?? [];
|
||||||
$settings = $state['settings'] ?? [];
|
$settings = $state['settings'] ?? [];
|
||||||
$settingsTable = $state['settings_table'] ?? null;
|
$settingsTable = $state['settings_table'] ?? null;
|
||||||
|
$policyType = $state['policy_type'] ?? null;
|
||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
@ -137,13 +138,102 @@
|
|||||||
@php
|
@php
|
||||||
$value = $entry['value'] ?? 'N/A';
|
$value = $entry['value'] ?? 'N/A';
|
||||||
|
|
||||||
|
$isScriptContent = in_array($entry['key'] ?? null, ['scriptContent', 'detectionScriptContent', 'remediationScriptContent'], true)
|
||||||
|
&& (bool) config('tenantpilot.display.show_script_content', false);
|
||||||
|
|
||||||
if (is_array($value) || is_object($value)) {
|
if (is_array($value) || is_object($value)) {
|
||||||
$value = json_encode($value, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
$value = json_encode($value, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||||
}
|
}
|
||||||
@endphp
|
@endphp
|
||||||
<span class="text-sm text-gray-900 dark:text-white break-words">
|
@if($isScriptContent)
|
||||||
{{ Str::limit((string) $value, 200) }}
|
@php
|
||||||
</span>
|
$code = (string) $value;
|
||||||
|
$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 !== '')
|
||||||
|
<style>
|
||||||
|
html.dark code.torchlight {
|
||||||
|
background-color: var(--phiki-dark-background-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark .phiki,
|
||||||
|
html.dark .phiki span {
|
||||||
|
color: var(--phiki-dark-color) !important;
|
||||||
|
font-style: var(--phiki-dark-font-style) !important;
|
||||||
|
font-weight: var(--phiki-dark-font-weight) !important;
|
||||||
|
text-decoration: var(--phiki-dark-text-decoration) !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
@else
|
||||||
|
<span class="text-sm text-gray-900 dark:text-white break-words">
|
||||||
|
{{ Str::limit((string) $value, 200) }}
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
@endforeach
|
@endforeach
|
||||||
|
|||||||
@ -4,17 +4,22 @@ # Tasks: Scripts Management (013)
|
|||||||
**Input**: [spec.md](./spec.md), [plan.md](./plan.md)
|
**Input**: [spec.md](./spec.md), [plan.md](./plan.md)
|
||||||
|
|
||||||
## Phase 1: Contracts Review
|
## Phase 1: Contracts Review
|
||||||
- [ ] T001 Verify `config/graph_contracts.php` entries for `deviceManagementScript`, `deviceShellScript`, `deviceHealthScript` (resource, type_family, assignment payload key).
|
- [x] T001 Verify `config/graph_contracts.php` entries for `deviceManagementScript`, `deviceShellScript`, `deviceHealthScript` (resource, type_family, assignment payload key).
|
||||||
|
|
||||||
## Phase 2: UI Normalization
|
## Phase 2: UI Normalization
|
||||||
- [ ] T002 Add a `ScriptsPolicyNormalizer` (or equivalent) to produce readable normalized settings for the three script policy types.
|
- [x] T002 Add a `ScriptsPolicyNormalizer` (or equivalent) to produce readable normalized settings for the three script policy types.
|
||||||
- [ ] T003 Register the normalizer in `AppServiceProvider`.
|
- [x] T003 Register the normalizer in `AppServiceProvider`.
|
||||||
|
|
||||||
## Phase 3: Tests + Verification
|
## Phase 3: Tests + Verification
|
||||||
- [ ] T004 Add tests for normalized output (shape + stability) for each script policy type.
|
- [x] T004 Add tests for normalized output (shape + stability) for each script policy type.
|
||||||
- [ ] T005 Add Filament render tests for “Normalized settings” tab for each script policy type.
|
- [x] T005 Add Filament render tests for “Normalized settings” tab for each script policy type.
|
||||||
- [ ] T006 Run targeted tests.
|
- [x] T006 Run targeted tests.
|
||||||
- [ ] T007 Run Pint (`./vendor/bin/pint --dirty`).
|
- [x] T007 Run Pint (`./vendor/bin/pint --dirty`).
|
||||||
|
|
||||||
|
## Phase 4: Script Content Display (Safe)
|
||||||
|
- [x] T008 Add opt-in display + base64 decoding for `scriptContent` in normalized settings.
|
||||||
|
- [x] T009 Highlight script content with Torch (shebang-based shell + PowerShell default).
|
||||||
|
- [x] T010 Hide script content behind a Show/Hide button (collapsed by default).
|
||||||
|
|
||||||
## Open TODOs (Follow-up)
|
## Open TODOs (Follow-up)
|
||||||
- None yet.
|
- None yet.
|
||||||
|
|||||||
@ -26,7 +26,16 @@
|
|||||||
'external_id' => 'policy-1',
|
'external_id' => 'policy-1',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
PolicyVersion::factory()->create([
|
config([
|
||||||
|
'tenantpilot.display.show_script_content' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$scriptContent = str_repeat('X', 20);
|
||||||
|
if ($policyType === 'deviceShellScript') {
|
||||||
|
$scriptContent = "#!/bin/zsh\n".str_repeat('X', 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
$version = PolicyVersion::factory()->create([
|
||||||
'policy_id' => $policy->id,
|
'policy_id' => $policy->id,
|
||||||
'tenant_id' => $tenant->id,
|
'tenant_id' => $tenant->id,
|
||||||
'policy_type' => $policyType,
|
'policy_type' => $policyType,
|
||||||
@ -34,13 +43,16 @@
|
|||||||
'@odata.type' => $odataType,
|
'@odata.type' => $odataType,
|
||||||
'displayName' => 'Script policy',
|
'displayName' => 'Script policy',
|
||||||
'description' => 'desc',
|
'description' => 'desc',
|
||||||
'scriptContent' => str_repeat('X', 20),
|
'scriptContent' => $scriptContent,
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->get(\App\Filament\Resources\PolicyVersionResource::getUrl('index'))
|
$this->get(\App\Filament\Resources\PolicyVersionResource::getUrl('index'))
|
||||||
->assertSuccessful();
|
->assertSuccessful();
|
||||||
|
|
||||||
|
$this->get(\App\Filament\Resources\PolicyVersionResource::getUrl('view', ['record' => $version]).'?tab=normalized-settings')
|
||||||
|
->assertSuccessful();
|
||||||
|
|
||||||
$originalEnv !== false
|
$originalEnv !== false
|
||||||
? putenv("INTUNE_TENANT_ID={$originalEnv}")
|
? putenv("INTUNE_TENANT_ID={$originalEnv}")
|
||||||
: putenv('INTUNE_TENANT_ID');
|
: putenv('INTUNE_TENANT_ID');
|
||||||
|
|||||||
@ -56,3 +56,113 @@
|
|||||||
expect($result['settings'])->toBeArray()->not->toBeEmpty();
|
expect($result['settings'])->toBeArray()->not->toBeEmpty();
|
||||||
expect($result['settings'][0]['type'])->toBe('keyValue');
|
expect($result['settings'][0]['type'])->toBe('keyValue');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('summarizes script content by default', function () {
|
||||||
|
config([
|
||||||
|
'tenantpilot.display.show_script_content' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$normalizer = app(PolicyNormalizer::class);
|
||||||
|
|
||||||
|
$snapshot = [
|
||||||
|
'@odata.type' => '#microsoft.graph.deviceManagementScript',
|
||||||
|
'displayName' => 'My PS script',
|
||||||
|
'scriptContent' => 'ABC',
|
||||||
|
];
|
||||||
|
|
||||||
|
$result = $normalizer->normalize($snapshot, 'deviceManagementScript', 'windows');
|
||||||
|
|
||||||
|
$entries = collect($result['settings'][0]['entries']);
|
||||||
|
|
||||||
|
expect($entries->firstWhere('key', 'scriptContent')['value'])->toBe('[content: 3 chars]');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows script content when enabled', function () {
|
||||||
|
config([
|
||||||
|
'tenantpilot.display.show_script_content' => true,
|
||||||
|
'tenantpilot.display.max_script_content_chars' => 100,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$normalizer = app(PolicyNormalizer::class);
|
||||||
|
|
||||||
|
$snapshot = [
|
||||||
|
'@odata.type' => '#microsoft.graph.deviceManagementScript',
|
||||||
|
'displayName' => 'My PS script',
|
||||||
|
'scriptContent' => "line1\nline2",
|
||||||
|
];
|
||||||
|
|
||||||
|
$result = $normalizer->normalize($snapshot, 'deviceManagementScript', 'windows');
|
||||||
|
|
||||||
|
$entries = collect($result['settings'][0]['entries']);
|
||||||
|
|
||||||
|
expect($entries->firstWhere('key', 'scriptContent')['value'])->toBe("line1\nline2");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('decodes scriptContentBase64 when enabled and scriptContent is missing', function () {
|
||||||
|
config([
|
||||||
|
'tenantpilot.display.show_script_content' => true,
|
||||||
|
'tenantpilot.display.max_script_content_chars' => 50,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$normalizer = app(PolicyNormalizer::class);
|
||||||
|
|
||||||
|
$snapshot = [
|
||||||
|
'@odata.type' => '#microsoft.graph.deviceShellScript',
|
||||||
|
'displayName' => 'My macOS shell script',
|
||||||
|
'scriptContentBase64' => base64_encode('echo hello'),
|
||||||
|
];
|
||||||
|
|
||||||
|
$result = $normalizer->normalize($snapshot, 'deviceShellScript', 'macOS');
|
||||||
|
|
||||||
|
$entries = collect($result['settings'][0]['entries']);
|
||||||
|
|
||||||
|
expect($entries->firstWhere('key', 'scriptContent')['value'])->toBe('echo hello');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('decodes base64-looking scriptContent when enabled', function () {
|
||||||
|
config([
|
||||||
|
'tenantpilot.display.show_script_content' => true,
|
||||||
|
'tenantpilot.display.max_script_content_chars' => 5000,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$normalizer = app(PolicyNormalizer::class);
|
||||||
|
|
||||||
|
$plain = "# hello\nWrite-Host \"hi\"";
|
||||||
|
$snapshot = [
|
||||||
|
'@odata.type' => '#microsoft.graph.deviceManagementScript',
|
||||||
|
'displayName' => 'My PS script',
|
||||||
|
'scriptContent' => base64_encode($plain),
|
||||||
|
];
|
||||||
|
|
||||||
|
$result = $normalizer->normalize($snapshot, 'deviceManagementScript', 'windows');
|
||||||
|
|
||||||
|
$entries = collect($result['settings'][0]['entries']);
|
||||||
|
|
||||||
|
expect($entries->firstWhere('key', 'scriptContent')['value'])->toBe($plain);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('decodes base64-looking detection/remediation script content when enabled', function () {
|
||||||
|
config([
|
||||||
|
'tenantpilot.display.show_script_content' => true,
|
||||||
|
'tenantpilot.display.max_script_content_chars' => 5000,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$normalizer = app(PolicyNormalizer::class);
|
||||||
|
|
||||||
|
$detection = "# detection\nWrite-Host \"detect\"";
|
||||||
|
$remediation = "# remediation\nWrite-Host \"fix\"";
|
||||||
|
|
||||||
|
$snapshot = [
|
||||||
|
'@odata.type' => '#microsoft.graph.deviceHealthScript',
|
||||||
|
'displayName' => 'My remediation',
|
||||||
|
'detectionScriptContent' => base64_encode($detection),
|
||||||
|
'remediationScriptContent' => base64_encode($remediation),
|
||||||
|
];
|
||||||
|
|
||||||
|
$result = $normalizer->normalize($snapshot, 'deviceHealthScript', 'windows');
|
||||||
|
|
||||||
|
$entries = collect($result['settings'][0]['entries']);
|
||||||
|
|
||||||
|
expect($entries->firstWhere('key', 'detectionScriptContent')['value'])->toBe($detection);
|
||||||
|
expect($entries->firstWhere('key', 'remediationScriptContent')['value'])->toBe($remediation);
|
||||||
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user