>, settings_table?: array, warnings: array} */ public function normalize(?array $snapshot, string $policyType, ?string $platform = null): array { $snapshot = is_array($snapshot) ? $snapshot : []; $displayName = Arr::get($snapshot, 'displayName') ?? Arr::get($snapshot, 'name'); $description = Arr::get($snapshot, 'description'); $entries = []; $entries[] = ['key' => 'Type', 'value' => $policyType]; if (is_string($displayName) && $displayName !== '') { $entries[] = ['key' => 'Display name', 'value' => $displayName]; } if (is_string($description) && $description !== '') { $entries[] = ['key' => 'Description', 'value' => $description]; } $fileName = Arr::get($snapshot, 'fileName'); if (is_string($fileName) && $fileName !== '') { $entries[] = ['key' => 'File name', 'value' => $fileName]; } $publisher = Arr::get($snapshot, 'publisher'); if (is_string($publisher) && $publisher !== '') { $entries[] = ['key' => 'Publisher', 'value' => $publisher]; } $runAsAccount = Arr::get($snapshot, 'runAsAccount'); if (is_string($runAsAccount) && $runAsAccount !== '') { $entries[] = ['key' => 'Run as account', 'value' => $runAsAccount]; } $runAs32Bit = Arr::get($snapshot, 'runAs32Bit'); if (is_bool($runAs32Bit)) { $entries[] = ['key' => 'Run as 32-bit', 'value' => $runAs32Bit ? 'Enabled' : 'Disabled']; } $enforceSignatureCheck = Arr::get($snapshot, 'enforceSignatureCheck'); if (is_bool($enforceSignatureCheck)) { $entries[] = ['key' => 'Enforce signature check', 'value' => $enforceSignatureCheck ? 'Enabled' : 'Disabled']; } $entries = array_merge($entries, $this->contentEntries($snapshot)); $schedule = Arr::get($snapshot, 'runSchedule'); if (is_array($schedule) && $schedule !== []) { $entries[] = ['key' => 'Run schedule', 'value' => Arr::except($schedule, ['@odata.type'])]; } $frequency = Arr::get($snapshot, 'runFrequency'); if (is_string($frequency) && $frequency !== '') { $entries[] = ['key' => 'Run frequency', 'value' => $frequency]; } $roleScopeTagIds = Arr::get($snapshot, 'roleScopeTagIds'); if (is_array($roleScopeTagIds) && $roleScopeTagIds !== []) { $entries[] = ['key' => 'Scope tag IDs', 'value' => array_values($roleScopeTagIds)]; } return [ 'status' => 'ok', 'settings' => [ [ 'type' => 'keyValue', 'title' => 'Script settings', 'entries' => $entries, ], ], 'warnings' => [], ]; } /** * @return array */ 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 */ 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 */ public function flattenForDiff(?array $snapshot, string $policyType, ?string $platform = null): array { $normalized = $this->normalize($snapshot, $policyType, $platform); return $this->defaultNormalizer->flattenNormalizedForDiff($normalized); } }