Assets/RemediationLibrary/update-source-migration/Review-UpdatePolicies.ps1

#requires -Version 7.0
<#
.SYNOPSIS
Connects endpoint update observations to explicitly mapped tenant policy settings.
.DESCRIPTION
Read-only analyst review. Exact setting mappings are administrator supplied.
Targeting and a matching policy setting do not prove effective enforcement.
#>

[CmdletBinding()]
param(
    [Parameter(Mandatory)][string]$EvidencePath,
    [Parameter(Mandatory)][string]$SnapshotPath,
    [Parameter(Mandatory)][ValidateNotNullOrEmpty()][string]$TenantId,
    [Parameter(Mandatory)][ValidateNotNullOrEmpty()][string]$DeviceId,
    [ValidateRange(1,720)][int]$MaximumAgeHours=24
)
$ErrorActionPreference='Stop'
$parser=Join-Path $PSScriptRoot 'ConvertFrom-IntuneAccessSnapshotJson.ps1'
if(-not (Test-Path -LiteralPath $parser)){$parser=Join-Path $PSScriptRoot '../../../Private/ConvertFrom-IntuneAccessSnapshotJson.ps1'}
. $parser
. (Join-Path (Split-Path $parser) 'Get-IntuneAccessByteHash.ps1')
function Read-ReviewJson {
    param($Path)
    $file=Get-Item -LiteralPath $Path
    if($file.Length -gt 52428800){throw 'Input exceeds 50 MiB.'}
    ConvertFrom-IntuneAccessSnapshotJson -Json (Get-Content -LiteralPath $file.FullName -Raw)
}
$snapshot=Read-ReviewJson $SnapshotPath
if($snapshot.SchemaVersion -ne '2.0' -or $snapshot.Schema -ne 'https://controlaltdeletetechbits.github.io/intune-access/schemas/snapshot-2.0.json' -or $snapshot.IdentityMode -ne 'Full'){throw 'Use a full-identity schema 2.0 snapshot.'}
if($snapshot.Tenant.Id -cne $TenantId){throw 'Snapshot tenant does not match the selected tenant.'}
$bytes=[Text.Encoding]::UTF8.GetBytes(($snapshot.Data | ConvertTo-Json -Depth 40 -Compress))
$hash=Get-IntuneAccessByteHash -Bytes $bytes
if($hash -cne $snapshot.IntegritySha256){throw 'Snapshot integrity validation failed.'}
$endpoint=Read-ReviewJson $EvidencePath
if($endpoint.Kind -ne 'IntuneAccess.EndpointEvidence' -or $endpoint.SchemaVersion -ne '1.0' -or $endpoint.Investigation -ne 'UpdateSources'){throw 'Use update-source endpoint evidence.'}
$mappings=@($endpoint.Configuration.PolicyMappings)
if($mappings.Count -gt 50){throw 'At most 50 policy mappings are supported.'}
$seen=@{}
foreach($mapping in $mappings){
    if($null -eq $mapping){continue}
    if($mapping.Category -notin @('Quality','Feature','Driver','Other') -or [string]::IsNullOrWhiteSpace($mapping.SettingDefinitionId)){throw 'Policy mappings require an update category and exact setting definition ID.'}
    if($seen.ContainsKey([string]$mapping.SettingDefinitionId)){throw 'Setting definition IDs must be unique across mappings.'}
    $seen[[string]$mapping.SettingDefinitionId]=$true
}
$fresh=$true
foreach($stamp in @($snapshot.Data.CollectedAt,$endpoint.CollectedAt)){
    $parsed=[DateTimeOffset]::MinValue
    if(-not [DateTimeOffset]::TryParse([string]$stamp,[ref]$parsed)){$fresh=$false;continue}
    $hours=([DateTimeOffset]::UtcNow-$parsed).TotalHours
    if($hours -lt -0.083333 -or $hours -gt $MaximumAgeHours){$fresh=$false}
}
$reviewer=Join-Path $PSScriptRoot 'Review-Evidence.ps1'
if(-not (Test-Path -LiteralPath $reviewer)){$reviewer=Join-Path $PSScriptRoot '../investigation-common/Review-Evidence.ps1'}
$review=& $reviewer -EvidencePath $EvidencePath | ConvertFrom-Json -Depth 40
foreach($finding in $review.Findings){
    $ids=@($mappings | Where-Object Category -EQ $finding.Id | ForEach-Object SettingDefinitionId)
    $candidates=@(foreach($setting in @($snapshot.Data.PolicySettings | Where-Object {$_.SettingDefinitionId -cin $ids})){
        $status=@($snapshot.Data.PolicyConflictCollectionStatus | Where-Object WorkloadId -CEQ $setting.WorkloadId)
        $assignment=@($snapshot.Data.DeviceAssignmentExplanations | Where-Object {$_.DeviceId -ceq $DeviceId -and $_.WorkloadId -ceq $setting.WorkloadId})
        $state='NotEvaluated'
        if($fresh -and $status.Count -eq 1 -and $status[0].State -eq 'Available' -and $assignment.Count -eq 1){
            if($assignment[0].AssignmentState -eq 'Included'){$state='TargetedPolicyCandidate'}
            elseif($assignment[0].AssignmentState -eq 'Excluded'){$state='ExcludedInSnapshot'}
        }
        [pscustomobject]@{
            PolicyId=$setting.WorkloadId;PolicyName=$setting.WorkloadName
            SettingDefinitionId=$setting.SettingDefinitionId;ConfiguredValueJson=$setting.ValueJson
            SourceEndpoint=$setting.SourceEndpoint;SourceApiVersion=$setting.SourceApiVersion
            State=$state;Assignments=$assignment;CollectionStatus=$status
            NextStep=if($state -eq 'TargetedPolicyCandidate'){'Open this exact policy and verify the mapped setting and intended source. Review competing GPO and co-management authority before a pilot change.'}
                elseif($state -eq 'ExcludedInSnapshot'){'This snapshot excludes the device. Do not propose changing this policy as its current owner without additional evidence.'}
                else {'Resolve stale, missing or ambiguous collection and assignment evidence before selecting this policy for correction.'}
        }
    })
    $finding.Proposal | Add-Member -NotePropertyName TenantPolicyCandidates -NotePropertyValue $candidates
    $finding.Proposal | Add-Member -NotePropertyName MappingProvenance -NotePropertyValue 'Administrator-supplied exact category/definition mapping; verify against the policy definition before changing anything.'
    $finding.Proposal | Add-Member -NotePropertyName TenantCorrelation -NotePropertyValue $(if(-not $fresh){'NotEvaluated: stale or invalid collection time'}elseif(-not $ids.Count){'NotEvaluated: no exact mapping supplied'}elseif(-not $candidates.Count){'No matching setting observed; absence does not establish absence of policy ownership'}else{'Candidates only; targeting is not proof of effective ownership'})
}
$review | Add-Member -NotePropertyName TenantId -NotePropertyValue $TenantId
$review | Add-Member -NotePropertyName SelectedDeviceId -NotePropertyValue $DeviceId
$review | Add-Member -NotePropertyName IdentityVerified -NotePropertyValue $false
$review | Add-Member -NotePropertyName SnapshotCollectedAt -NotePropertyValue $snapshot.Data.CollectedAt
$review | ConvertTo-Json -Depth 40