407 lines
13 KiB
PHP
407 lines
13 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Intune;
|
|
|
|
use DateInterval;
|
|
use Illuminate\Support\Str;
|
|
|
|
class AppProtectionPolicyNormalizer implements PolicyTypeNormalizer
|
|
{
|
|
public function __construct(
|
|
private readonly DefaultPolicyNormalizer $defaultNormalizer,
|
|
) {}
|
|
|
|
public function supports(string $policyType): bool
|
|
{
|
|
return $policyType === 'appProtectionPolicy';
|
|
}
|
|
|
|
/**
|
|
* @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->buildBlocks($snapshot) as $block) {
|
|
$normalized['settings'][] = $block;
|
|
}
|
|
|
|
return $normalized;
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
public function flattenForDiff(?array $snapshot, string $policyType, ?string $platform = null): array
|
|
{
|
|
$snapshot = $snapshot ?? [];
|
|
$normalized = $this->normalize($snapshot, $policyType, $platform);
|
|
|
|
return $this->defaultNormalizer->flattenNormalizedForDiff($normalized);
|
|
}
|
|
|
|
/**
|
|
* @return array<int, array<string, mixed>>
|
|
*/
|
|
private function buildBlocks(array $snapshot): array
|
|
{
|
|
$blocks = [];
|
|
$groups = $this->groupedFields();
|
|
$usedKeys = [];
|
|
|
|
foreach ($groups as $title => $group) {
|
|
$rows = $this->buildRows($snapshot, $group['keys'], $group['labels'] ?? []);
|
|
|
|
if ($rows === []) {
|
|
continue;
|
|
}
|
|
|
|
if ($title === 'Basics') {
|
|
$platformLabel = $this->platformLabelFromOdataType($snapshot['@odata.type'] ?? null);
|
|
|
|
if ($platformLabel !== null) {
|
|
array_unshift($rows, [
|
|
'path' => '@odata.type',
|
|
'label' => 'Platform',
|
|
'value' => $platformLabel,
|
|
]);
|
|
}
|
|
}
|
|
|
|
$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 [
|
|
'Basics' => [
|
|
'keys' => [
|
|
'displayName',
|
|
'description',
|
|
'appGroupType',
|
|
'isAssigned',
|
|
'deployedAppCount',
|
|
],
|
|
'labels' => [
|
|
'displayName' => 'Name',
|
|
'description' => 'Description',
|
|
'appGroupType' => 'App group type',
|
|
'isAssigned' => 'Assigned',
|
|
'deployedAppCount' => 'Deployed app count',
|
|
],
|
|
],
|
|
'Data Protection' => [
|
|
'keys' => [
|
|
'dataBackupBlocked',
|
|
'printBlocked',
|
|
'saveAsBlocked',
|
|
'screenCaptureBlocked',
|
|
'allowedInboundDataTransferSources',
|
|
'allowedOutboundDataTransferDestinations',
|
|
'allowedDataIngestionLocations',
|
|
'allowedOutboundClipboardSharingLevel',
|
|
],
|
|
'labels' => [
|
|
'dataBackupBlocked' => 'Prevent backups',
|
|
'printBlocked' => 'Printing org data',
|
|
'saveAsBlocked' => 'Save copies of org data',
|
|
'screenCaptureBlocked' => 'Screen capture',
|
|
'allowedInboundDataTransferSources' => 'Receive data from other apps',
|
|
'allowedOutboundDataTransferDestinations' => 'Send org data to other apps',
|
|
'allowedDataIngestionLocations' => 'Allow users to open data from selected services',
|
|
'allowedOutboundClipboardSharingLevel' => 'Restrict cut, copy, and paste',
|
|
],
|
|
],
|
|
'Access Requirements' => [
|
|
'keys' => [
|
|
'pinRequired',
|
|
'pinCharacterSet',
|
|
'minimumPinLength',
|
|
'simplePinBlocked',
|
|
'maximumPinRetries',
|
|
'fingerprintAndBiometricEnabled',
|
|
'pinRequiredInsteadOfBiometricTimeout',
|
|
'periodOnlineBeforeAccessCheck',
|
|
'periodOfflineBeforeAccessCheck',
|
|
],
|
|
'labels' => [
|
|
'pinRequired' => 'PIN for access',
|
|
'pinCharacterSet' => 'PIN type',
|
|
'minimumPinLength' => 'Minimum PIN length',
|
|
'simplePinBlocked' => 'Block simple PIN',
|
|
'maximumPinRetries' => 'Max PIN attempts',
|
|
'fingerprintAndBiometricEnabled' => 'Biometrics instead of PIN',
|
|
'pinRequiredInsteadOfBiometricTimeout' => 'Override biometrics with PIN after timeout',
|
|
'periodOnlineBeforeAccessCheck' => 'Recheck access requirements after',
|
|
'periodOfflineBeforeAccessCheck' => 'Offline grace period (block access)',
|
|
],
|
|
],
|
|
'Conditional Launch' => [
|
|
'keys' => [
|
|
'periodOfflineBeforeWipeIsEnforced',
|
|
'appActionIfMaximumPinRetriesExceeded',
|
|
'appActionIfDeviceLockNotSet',
|
|
'appActionIfDeviceComplianceRequired',
|
|
'maximumAllowedDeviceThreatLevel',
|
|
'mobileThreatDefenseRemediationAction',
|
|
],
|
|
'labels' => [
|
|
'periodOfflineBeforeWipeIsEnforced' => 'Offline grace period (wipe data)',
|
|
'appActionIfMaximumPinRetriesExceeded' => 'Action if max PIN retries exceeded',
|
|
'appActionIfDeviceLockNotSet' => 'Action if device lock not set',
|
|
'appActionIfDeviceComplianceRequired' => 'Action if device compliance required',
|
|
'maximumAllowedDeviceThreatLevel' => 'Maximum allowed device threat level',
|
|
'mobileThreatDefenseRemediationAction' => 'Threat defense remediation action',
|
|
],
|
|
],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @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[] = [
|
|
'path' => $key,
|
|
'label' => $labels[$key] ?? Str::headline($key),
|
|
'value' => $this->formatValue($key, $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[] = [
|
|
'path' => $key,
|
|
'label' => Str::headline($key),
|
|
'value' => $this->formatValue($key, $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',
|
|
];
|
|
}
|
|
|
|
private function formatValue(string $key, mixed $value): mixed
|
|
{
|
|
if (is_bool($value)) {
|
|
$normalized = strtolower($key);
|
|
|
|
if (str_ends_with($normalized, 'blocked')) {
|
|
return $value ? 'Blocked' : 'Allowed';
|
|
}
|
|
|
|
if (str_ends_with($normalized, 'required')) {
|
|
return $value ? 'Required' : 'Not required';
|
|
}
|
|
|
|
if (str_contains($normalized, 'enabled')) {
|
|
return $value ? 'Enabled' : 'Disabled';
|
|
}
|
|
|
|
return $value ? 'Yes' : 'No';
|
|
}
|
|
|
|
if (is_array($value)) {
|
|
if ($value === []) {
|
|
return 'None';
|
|
}
|
|
|
|
if (array_is_list($value) && $this->isScalarList($value)) {
|
|
return implode(', ', array_map(fn (mixed $item) => $this->formatScalarListItem($item), $value));
|
|
}
|
|
|
|
return json_encode($value, JSON_PRETTY_PRINT);
|
|
}
|
|
|
|
if (is_string($value)) {
|
|
$duration = $this->formatDuration($value);
|
|
|
|
if ($duration !== null) {
|
|
return $duration;
|
|
}
|
|
|
|
if ($value === '') {
|
|
return null;
|
|
}
|
|
|
|
if (preg_match('/^[a-zA-Z][a-zA-Z0-9]*$/', $value) === 1) {
|
|
return Str::headline($value);
|
|
}
|
|
|
|
return $value;
|
|
}
|
|
|
|
return $value;
|
|
}
|
|
|
|
/**
|
|
* @param array<int, mixed> $value
|
|
*/
|
|
private function isScalarList(array $value): bool
|
|
{
|
|
foreach ($value as $item) {
|
|
if (! is_string($item) && ! is_int($item) && ! is_float($item)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private function formatScalarListItem(mixed $value): string
|
|
{
|
|
if (is_int($value) || is_float($value)) {
|
|
return (string) $value;
|
|
}
|
|
|
|
if (! is_string($value)) {
|
|
return '';
|
|
}
|
|
|
|
if (preg_match('/^[a-zA-Z][a-zA-Z0-9]*$/', $value) === 1) {
|
|
return Str::headline($value);
|
|
}
|
|
|
|
return $value;
|
|
}
|
|
|
|
private function formatDuration(string $value): ?string
|
|
{
|
|
if (! preg_match('/^P[T0-9]/i', $value)) {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
$interval = new DateInterval(strtoupper($value));
|
|
} catch (\Throwable) {
|
|
return null;
|
|
}
|
|
|
|
$parts = [];
|
|
|
|
if ($interval->y) {
|
|
$parts[] = $interval->y.' '.Str::plural('year', $interval->y);
|
|
}
|
|
if ($interval->m) {
|
|
$parts[] = $interval->m.' '.Str::plural('month', $interval->m);
|
|
}
|
|
if ($interval->d) {
|
|
$parts[] = $interval->d.' '.Str::plural('day', $interval->d);
|
|
}
|
|
|
|
if ($interval->h) {
|
|
$parts[] = $interval->h.' '.Str::plural('hour', $interval->h);
|
|
}
|
|
if ($interval->i) {
|
|
$parts[] = $interval->i.' '.Str::plural('minute', $interval->i);
|
|
}
|
|
if ($interval->s && $parts === []) {
|
|
$parts[] = $interval->s.' '.Str::plural('second', $interval->s);
|
|
}
|
|
|
|
if ($parts === []) {
|
|
return null;
|
|
}
|
|
|
|
return implode(' ', $parts);
|
|
}
|
|
|
|
private function platformLabelFromOdataType(mixed $odataType): ?string
|
|
{
|
|
if (! is_string($odataType) || $odataType === '') {
|
|
return null;
|
|
}
|
|
|
|
return match (strtolower($odataType)) {
|
|
'#microsoft.graph.androidmanagedappprotection' => 'Android',
|
|
'#microsoft.graph.iosmanagedappprotection' => 'iOS',
|
|
'#microsoft.graph.windowsinformationprotectionpolicy',
|
|
'#microsoft.graph.mdmwindowsinformationprotectionpolicy' => 'Windows',
|
|
default => null,
|
|
};
|
|
}
|
|
}
|