TenantAtlas/app/Services/Intune/CompliancePolicyNormalizer.php
2025-12-27 22:32:51 +01:00

297 lines
10 KiB
PHP

<?php
namespace App\Services\Intune;
use Illuminate\Support\Str;
class CompliancePolicyNormalizer implements PolicyTypeNormalizer
{
public function __construct(
private readonly DefaultPolicyNormalizer $defaultNormalizer,
) {}
public function supports(string $policyType): bool
{
return $policyType === 'deviceCompliancePolicy';
}
/**
* @return array{status: string, settings: array<int, array<string, mixed>>, settings_table?: array<string, mixed>, warnings: array<int, string>}
*/
public function normalize(?array $snapshot, string $policyType, ?string $platform = null): array
{
$snapshot = $snapshot ?? [];
$normalized = $this->defaultNormalizer->normalize($snapshot, $policyType, $platform);
if ($snapshot === []) {
return $normalized;
}
$normalized['settings'] = array_values(array_filter(
$normalized['settings'],
fn (array $block) => strtolower((string) ($block['title'] ?? '')) !== 'general'
));
foreach ($this->buildComplianceBlocks($snapshot) as $block) {
$normalized['settings'][] = $block;
}
return $normalized;
}
/**
* @return array<string, mixed>
*/
public function flattenForDiff(?array $snapshot, string $policyType, ?string $platform = null): array
{
return $this->defaultNormalizer->flattenForDiff($snapshot, $policyType, $platform);
}
/**
* @return array<int, array<string, mixed>>
*/
private function buildComplianceBlocks(array $snapshot): array
{
$blocks = [];
$groups = $this->groupedFields();
$usedKeys = [];
foreach ($groups as $title => $group) {
$rows = $this->buildRows($snapshot, $group['keys'], $group['labels'] ?? []);
if ($rows === []) {
continue;
}
$blocks[] = [
'type' => 'table',
'title' => $title,
'rows' => $rows,
];
$usedKeys = array_merge($usedKeys, $group['keys']);
}
$additionalRows = $this->buildAdditionalRows($snapshot, $usedKeys);
if ($additionalRows !== []) {
$blocks[] = [
'type' => 'table',
'title' => 'Additional Settings',
'rows' => $additionalRows,
];
}
return $blocks;
}
/**
* @return array{keys: array<int, string>, labels?: array<string, string>}
*/
private function groupedFields(): array
{
return [
'Password & Access' => [
'keys' => [
'passwordRequired',
'passwordRequiredType',
'passwordBlockSimple',
'passwordMinimumLength',
'passwordMinimumCharacterSetCount',
'passwordExpirationDays',
'passwordMinutesOfInactivityBeforeLock',
'passwordPreviousPasswordBlockCount',
'passwordRequiredToUnlockFromIdle',
],
'labels' => [
'passwordRequired' => 'Password required',
'passwordRequiredType' => 'Password required type',
'passwordBlockSimple' => 'Block simple passwords',
'passwordMinimumLength' => 'Password minimum length',
'passwordMinimumCharacterSetCount' => 'Password minimum character set count',
'passwordExpirationDays' => 'Password expiration days',
'passwordMinutesOfInactivityBeforeLock' => 'Password idle lock (minutes)',
'passwordPreviousPasswordBlockCount' => 'Password history count',
'passwordRequiredToUnlockFromIdle' => 'Password required to unlock from idle',
],
],
'Defender & Threat Protection' => [
'keys' => [
'defenderEnabled',
'defenderVersion',
'antivirusRequired',
'antiSpywareRequired',
'rtpEnabled',
'signatureOutOfDate',
'deviceThreatProtectionEnabled',
'deviceThreatProtectionRequiredSecurityLevel',
'requireHealthyDeviceReport',
],
'labels' => [
'defenderEnabled' => 'Microsoft Defender enabled',
'defenderVersion' => 'Defender version',
'antivirusRequired' => 'Antivirus required',
'antiSpywareRequired' => 'Anti-spyware required',
'rtpEnabled' => 'Real-time protection enabled',
'signatureOutOfDate' => 'Signature out of date (days)',
'deviceThreatProtectionEnabled' => 'Device threat protection enabled',
'deviceThreatProtectionRequiredSecurityLevel' => 'Threat protection required level',
'requireHealthyDeviceReport' => 'Require healthy device report',
],
],
'Encryption & Integrity' => [
'keys' => [
'bitLockerEnabled',
'storageRequireEncryption',
'tpmRequired',
'secureBootEnabled',
'codeIntegrityEnabled',
'memoryIntegrityEnabled',
'kernelDmaProtectionEnabled',
'firmwareProtectionEnabled',
'virtualizationBasedSecurityEnabled',
'earlyLaunchAntiMalwareDriverEnabled',
],
'labels' => [
'bitLockerEnabled' => 'BitLocker required',
'storageRequireEncryption' => 'Storage encryption required',
'tpmRequired' => 'TPM required',
'secureBootEnabled' => 'Secure boot required',
'codeIntegrityEnabled' => 'Code integrity required',
'memoryIntegrityEnabled' => 'Memory integrity required',
'kernelDmaProtectionEnabled' => 'Kernel DMA protection required',
'firmwareProtectionEnabled' => 'Firmware protection required',
'virtualizationBasedSecurityEnabled' => 'Virtualization-based security required',
'earlyLaunchAntiMalwareDriverEnabled' => 'Early launch anti-malware required',
],
],
'Operating System' => [
'keys' => [
'osMinimumVersion',
'osMaximumVersion',
'mobileOsMinimumVersion',
'mobileOsMaximumVersion',
'validOperatingSystemBuildRanges',
'wslDistributions',
],
'labels' => [
'osMinimumVersion' => 'OS minimum version',
'osMaximumVersion' => 'OS maximum version',
'mobileOsMinimumVersion' => 'Mobile OS minimum version',
'mobileOsMaximumVersion' => 'Mobile OS maximum version',
'validOperatingSystemBuildRanges' => 'Valid OS build ranges',
'wslDistributions' => 'Allowed WSL distributions',
],
],
'Firewall' => [
'keys' => [
'activeFirewallRequired',
],
'labels' => [
'activeFirewallRequired' => 'Active firewall required',
],
],
'Compliance Signals' => [
'keys' => [
'configurationManagerComplianceRequired',
'deviceCompliancePolicyScript',
],
'labels' => [
'configurationManagerComplianceRequired' => 'ConfigMgr compliance required',
'deviceCompliancePolicyScript' => 'Compliance policy script',
],
],
];
}
/**
* @param array<string, mixed> $labels
* @return array<int, array<string, mixed>>
*/
private function buildRows(array $snapshot, array $keys, array $labels = []): array
{
$rows = [];
foreach ($keys as $key) {
if (! array_key_exists($key, $snapshot)) {
continue;
}
$rows[] = [
'label' => $labels[$key] ?? Str::headline($key),
'value' => $this->formatValue($snapshot[$key]),
];
}
return $rows;
}
/**
* @param array<int, string> $usedKeys
* @return array<int, array<string, mixed>>
*/
private function buildAdditionalRows(array $snapshot, array $usedKeys): array
{
$ignoredKeys = array_merge($this->ignoredKeys(), $usedKeys);
$rows = [];
foreach ($snapshot as $key => $value) {
if (! is_string($key)) {
continue;
}
if (in_array($key, $ignoredKeys, true)) {
continue;
}
$rows[] = [
'label' => Str::headline($key),
'value' => $this->formatValue($value),
];
}
return $rows;
}
/**
* @return array<int, string>
*/
private function ignoredKeys(): array
{
return [
'@odata.context',
'@odata.type',
'id',
'version',
'createdDateTime',
'lastModifiedDateTime',
'supportsScopeTags',
'roleScopeTagIds',
'assignments',
'createdBy',
'lastModifiedBy',
'omaSettings',
'settings',
'settingsDelta',
'displayName',
'description',
'name',
'platform',
'platforms',
'technologies',
'settingCount',
'settingsCount',
'templateReference',
];
}
private function formatValue(mixed $value): mixed
{
if (is_array($value)) {
return json_encode($value, JSON_PRETTY_PRINT);
}
return $value;
}
}