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
260 lines
9.3 KiB
PHP
260 lines
9.3 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Intune;
|
|
|
|
use App\Models\Policy;
|
|
use App\Models\PolicyVersion;
|
|
use App\Models\Tenant;
|
|
use App\Services\Graph\AssignmentFetcher;
|
|
use App\Services\Graph\AssignmentFilterResolver;
|
|
use App\Services\Graph\GroupResolver;
|
|
use App\Services\Graph\ScopeTagResolver;
|
|
use Carbon\CarbonImmutable;
|
|
|
|
class VersionService
|
|
{
|
|
public function __construct(
|
|
private readonly AuditLogger $auditLogger,
|
|
private readonly PolicySnapshotService $snapshotService,
|
|
private readonly AssignmentFetcher $assignmentFetcher,
|
|
private readonly GroupResolver $groupResolver,
|
|
private readonly AssignmentFilterResolver $assignmentFilterResolver,
|
|
private readonly ScopeTagResolver $scopeTagResolver,
|
|
) {}
|
|
|
|
public function captureVersion(
|
|
Policy $policy,
|
|
array $payload,
|
|
?string $createdBy = null,
|
|
array $metadata = [],
|
|
?array $assignments = null,
|
|
?array $scopeTags = null,
|
|
): PolicyVersion {
|
|
$versionNumber = $this->nextVersionNumber($policy);
|
|
|
|
$version = PolicyVersion::create([
|
|
'tenant_id' => $policy->tenant_id,
|
|
'policy_id' => $policy->id,
|
|
'version_number' => $versionNumber,
|
|
'policy_type' => $policy->policy_type,
|
|
'platform' => $policy->platform,
|
|
'created_by' => $createdBy,
|
|
'captured_at' => CarbonImmutable::now(),
|
|
'snapshot' => $payload,
|
|
'metadata' => $metadata,
|
|
'assignments' => $assignments,
|
|
'scope_tags' => $scopeTags,
|
|
'assignments_hash' => $assignments ? hash('sha256', json_encode($assignments)) : null,
|
|
'scope_tags_hash' => $scopeTags ? hash('sha256', json_encode($scopeTags)) : null,
|
|
]);
|
|
|
|
$this->auditLogger->log(
|
|
tenant: $policy->tenant,
|
|
action: 'policy.versioned',
|
|
context: [
|
|
'metadata' => [
|
|
'policy_id' => $policy->id,
|
|
'version_number' => $versionNumber,
|
|
],
|
|
],
|
|
actorEmail: $createdBy,
|
|
resourceType: 'policy',
|
|
resourceId: (string) $policy->id
|
|
);
|
|
|
|
return $version;
|
|
}
|
|
|
|
public function captureFromGraph(
|
|
Tenant $tenant,
|
|
Policy $policy,
|
|
?string $createdBy = null,
|
|
array $metadata = [],
|
|
bool $includeAssignments = true,
|
|
bool $includeScopeTags = true,
|
|
): PolicyVersion {
|
|
$graphOptions = $tenant->graphOptions();
|
|
$tenantIdentifier = $graphOptions['tenant'] ?? $tenant->graphTenantId() ?? (string) $tenant->getKey();
|
|
|
|
$snapshot = $this->snapshotService->fetch($tenant, $policy, $createdBy);
|
|
|
|
if (isset($snapshot['failure'])) {
|
|
$reason = $snapshot['failure']['reason'] ?? 'Unable to fetch policy snapshot';
|
|
|
|
throw new \RuntimeException($reason);
|
|
}
|
|
|
|
$payload = $snapshot['payload'];
|
|
$snapshotMetadata = is_array($snapshot['metadata'] ?? null) ? $snapshot['metadata'] : [];
|
|
$snapshotWarnings = is_array($snapshot['warnings'] ?? null) ? $snapshot['warnings'] : [];
|
|
$assignments = null;
|
|
$scopeTags = null;
|
|
$assignmentMetadata = [];
|
|
|
|
if ($includeAssignments) {
|
|
try {
|
|
$rawAssignments = $this->assignmentFetcher->fetch(
|
|
$policy->policy_type,
|
|
$tenantIdentifier,
|
|
$policy->external_id,
|
|
$graphOptions,
|
|
true,
|
|
$payload['@odata.type'] ?? null,
|
|
);
|
|
$assignmentMetadata['assignments_fetched'] = true;
|
|
$assignmentMetadata['assignments_count'] = count($rawAssignments);
|
|
|
|
if (! empty($rawAssignments)) {
|
|
$resolvedGroups = [];
|
|
|
|
// Resolve groups
|
|
$groupIds = collect($rawAssignments)
|
|
->pluck('target.groupId')
|
|
->filter()
|
|
->unique()
|
|
->values()
|
|
->toArray();
|
|
|
|
if (! empty($groupIds)) {
|
|
$resolvedGroups = $this->groupResolver->resolveGroupIds($groupIds, $tenantIdentifier, $graphOptions);
|
|
}
|
|
|
|
$assignmentMetadata['has_orphaned_assignments'] = collect($resolvedGroups)
|
|
->contains(fn (array $group) => $group['orphaned'] ?? false);
|
|
|
|
$filterIds = $this->extractAssignmentFilterIds($rawAssignments);
|
|
|
|
$filters = $this->assignmentFilterResolver->resolve($filterIds, $tenant);
|
|
$filterNames = collect($filters)
|
|
->pluck('displayName', 'id')
|
|
->all();
|
|
|
|
$assignments = $this->enrichAssignments($rawAssignments, $resolvedGroups, $filterNames);
|
|
}
|
|
} catch (\Throwable $e) {
|
|
$assignmentMetadata['assignments_fetch_failed'] = true;
|
|
$assignmentMetadata['assignments_fetch_error'] = $e->getMessage();
|
|
}
|
|
}
|
|
|
|
if ($includeScopeTags) {
|
|
$scopeTagIds = $payload['roleScopeTagIds'] ?? ['0'];
|
|
$scopeTags = $this->resolveScopeTags($tenant, $scopeTagIds);
|
|
}
|
|
|
|
$metadata = array_merge(
|
|
$snapshotMetadata,
|
|
['capture_source' => 'version_capture'],
|
|
$metadata,
|
|
$assignmentMetadata,
|
|
);
|
|
|
|
if ($snapshotWarnings !== []) {
|
|
$existingWarnings = is_array($metadata['warnings'] ?? null) ? $metadata['warnings'] : [];
|
|
$metadata['warnings'] = array_values(array_unique(array_merge($existingWarnings, $snapshotWarnings)));
|
|
}
|
|
|
|
return $this->captureVersion(
|
|
policy: $policy,
|
|
payload: $payload,
|
|
createdBy: $createdBy,
|
|
metadata: $metadata,
|
|
assignments: $assignments,
|
|
scopeTags: $scopeTags,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @param array<int, array<string, mixed>> $assignments
|
|
* @param array<string, array{id:string,displayName:?string,orphaned:bool}> $groups
|
|
* @param array<string, string> $filterNames
|
|
* @return array<int, array<string, mixed>>
|
|
*/
|
|
private function enrichAssignments(array $assignments, array $groups, array $filterNames): array
|
|
{
|
|
return array_map(function (array $assignment) use ($groups, $filterNames): array {
|
|
$target = $assignment['target'] ?? [];
|
|
$groupId = $target['groupId'] ?? null;
|
|
|
|
if ($groupId && isset($groups[$groupId])) {
|
|
$target['group_display_name'] = $groups[$groupId]['displayName'] ?? null;
|
|
$target['group_orphaned'] = $groups[$groupId]['orphaned'] ?? false;
|
|
}
|
|
|
|
$filterId = $assignment['deviceAndAppManagementAssignmentFilterId']
|
|
?? ($target['deviceAndAppManagementAssignmentFilterId'] ?? null);
|
|
$filterType = $assignment['deviceAndAppManagementAssignmentFilterType']
|
|
?? ($target['deviceAndAppManagementAssignmentFilterType'] ?? null);
|
|
|
|
if ($filterId) {
|
|
$target['deviceAndAppManagementAssignmentFilterId'] = $target['deviceAndAppManagementAssignmentFilterId'] ?? $filterId;
|
|
|
|
if ($filterType) {
|
|
$target['deviceAndAppManagementAssignmentFilterType'] = $target['deviceAndAppManagementAssignmentFilterType'] ?? $filterType;
|
|
}
|
|
|
|
if (isset($filterNames[$filterId])) {
|
|
$target['assignment_filter_name'] = $filterNames[$filterId];
|
|
}
|
|
}
|
|
|
|
$assignment['target'] = $target;
|
|
|
|
return $assignment;
|
|
}, $assignments);
|
|
}
|
|
|
|
/**
|
|
* @param array<int, array<string, mixed>> $assignments
|
|
* @return array<int, string>
|
|
*/
|
|
private function extractAssignmentFilterIds(array $assignments): array
|
|
{
|
|
$filterIds = [];
|
|
|
|
foreach ($assignments as $assignment) {
|
|
if (! is_array($assignment)) {
|
|
continue;
|
|
}
|
|
|
|
$filterId = $assignment['deviceAndAppManagementAssignmentFilterId']
|
|
?? ($assignment['target']['deviceAndAppManagementAssignmentFilterId'] ?? null);
|
|
|
|
if (is_string($filterId) && $filterId !== '') {
|
|
$filterIds[] = $filterId;
|
|
}
|
|
}
|
|
|
|
return array_values(array_unique($filterIds));
|
|
}
|
|
|
|
/**
|
|
* @param array<int, string> $scopeTagIds
|
|
* @return array{ids:array<int, string>,names:array<int, string>}
|
|
*/
|
|
private function resolveScopeTags(Tenant $tenant, array $scopeTagIds): array
|
|
{
|
|
$scopeTags = $this->scopeTagResolver->resolve($scopeTagIds, $tenant);
|
|
|
|
$names = [];
|
|
foreach ($scopeTagIds as $id) {
|
|
$scopeTag = collect($scopeTags)->firstWhere('id', $id);
|
|
$names[] = $scopeTag['displayName'] ?? ($id === '0' ? 'Default' : "Unknown (ID: {$id})");
|
|
}
|
|
|
|
return [
|
|
'ids' => $scopeTagIds,
|
|
'names' => $names,
|
|
];
|
|
}
|
|
|
|
private function nextVersionNumber(Policy $policy): int
|
|
{
|
|
$current = PolicyVersion::query()
|
|
->where('policy_id', $policy->id)
|
|
->max('version_number');
|
|
|
|
return (int) ($current ?? 0) + 1;
|
|
}
|
|
}
|