Public/Restore-IntuneScriptContent.ps1

# Auto-generated by module/Build-Module.ps1 from en/scripts/Recover-IntuneScriptContentsFromProd.ps1
# Edit the source script, then re-run Build-Module.ps1. Do not edit this file directly.

function Restore-IntuneScriptContent {
<#
.SYNOPSIS
    Attempts to rehydrate Intune PowerShell script and Remediation script contents in an export folder.

.DESCRIPTION
    The failed import showed scripts/remediations with missing scriptContent, detectionScriptContent,
    and remediationScriptContent. This script connects to the PROD/source tenant, reads each object by
    its exported source id, and writes any returned content back into a copy of the export folder.

    If Graph does not return content for some objects, the script records them in a CSV. Those objects
    must be restored from the source script repository or recreated manually.
#>

[CmdletBinding()]
param(
    [Parameter(Mandatory=$true)][string]$SourcePath,
    [Parameter(Mandatory=$true)][string]$SourceTenantId,
    [string]$OutputPath,
    [switch]$ForceOverwrite,
    [string]$LogPath = (Join-Path (Get-Location) ("RecoverScriptContents_{0}.csv" -f (Get-Date -Format 'yyyyMMdd_HHmmss')))
)

$ErrorActionPreference='Stop'
$GraphBase='https://graph.microsoft.com/beta'
$Results=New-Object System.Collections.Generic.List[object]
$Scopes=@('DeviceManagementScripts.ReadWrite.All','DeviceManagementConfiguration.ReadWrite.All','Organization.Read.All')


function Connect-GraphForIntuneAutomation {
    param(
        [Parameter(Mandatory=$true)][string]$TenantId,
        [string[]]$Scopes
    )
    Import-Module Microsoft.Graph.Authentication -ErrorAction Stop

    $tenantLower = $TenantId.ToLowerInvariant()
    $sourceTenant = $env:INTUNE_AUTO_SOURCE_TENANT_ID
    $targetTenant = $env:INTUNE_AUTO_TARGET_TENANT_ID

    $clientId = $null
    $certThumb = $null

    if ($sourceTenant -and ($tenantLower -eq $sourceTenant.ToLowerInvariant())) {
        $clientId = $env:INTUNE_AUTO_SOURCE_CLIENT_ID
        $certThumb = $env:INTUNE_AUTO_SOURCE_CERT_THUMBPRINT
    }
    if ($targetTenant -and ($tenantLower -eq $targetTenant.ToLowerInvariant())) {
        $clientId = $env:INTUNE_AUTO_TARGET_CLIENT_ID
        $certThumb = $env:INTUNE_AUTO_TARGET_CERT_THUMBPRINT
    }
    if (-not $clientId -and $env:INTUNE_AUTO_CLIENT_ID) { $clientId = $env:INTUNE_AUTO_CLIENT_ID }
    if (-not $certThumb -and $env:INTUNE_AUTO_CERT_THUMBPRINT) { $certThumb = $env:INTUNE_AUTO_CERT_THUMBPRINT }

    $ctx = Get-MgContext -ErrorAction SilentlyContinue

    if ($clientId -and $certThumb) {
        if ($ctx -and $ctx.TenantId -and ($ctx.TenantId.ToLowerInvariant() -eq $tenantLower) -and $ctx.ClientId -and ($ctx.ClientId -eq $clientId)) {
            return
        }
        Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null
        Connect-MgGraph -TenantId $TenantId -ClientId $clientId -CertificateThumbprint $certThumb -ContextScope Process -NoWelcome | Out-Null
        return
    }

    if ($ctx -and $ctx.TenantId -and ($ctx.TenantId.ToLowerInvariant() -eq $tenantLower)) {
        $ctxScopes = @($ctx.Scopes)
        $missing = @()
        foreach ($s in @($Scopes)) {
            if ($ctxScopes -notcontains $s) { $missing += $s }
        }
        if ($missing.Count -eq 0) { return }
        Write-Host ("Missing Graph scopes for {0}: {1}. Re-authentication required." -f $TenantId, ($missing -join ', ')) -ForegroundColor Yellow
        Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null
    } elseif ($ctx) {
        Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null
    }

    Connect-MgGraph -TenantId $TenantId -Scopes $Scopes -ContextScope Process -NoWelcome | Out-Null
}

function Add-Result($Family,$Name,$Id,$Status,$UpdatedFields,$Reason,$ErrorMessage){
 $Results.Add([pscustomobject]@{DateUtc=(Get-Date).ToUniversalTime().ToString('s')+'Z';Family=$Family;Name=$Name;Id=$Id;Status=$Status;UpdatedFields=$UpdatedFields;Reason=$Reason;ErrorMessage=$ErrorMessage}) | Out-Null
}
function Save-Results(){ if($Results.Count -gt 0){ $Results | Export-Csv -Path $LogPath -NoTypeInformation -Encoding UTF8; Write-Host "Log: $LogPath" -ForegroundColor Cyan } }
function Resolve-ExportRoot($Path){
 if(-not (Test-Path -LiteralPath $Path)){ throw "Source not found: $Path" }
 $item=Get-Item -LiteralPath $Path
 if($item.PSIsContainer){
  if(Test-Path -LiteralPath (Join-Path $item.FullName 'manifest.json')){ return $item.FullName }
  $mf=Get-ChildItem -Path $item.FullName -Filter manifest.json -Recurse -File | Select-Object -First 1
  if($mf){ return $mf.Directory.FullName }
 }
 if($item.Extension -ieq '.zip'){
  $tmp=Join-Path $env:TEMP ('IntuneRecover_' + [guid]::NewGuid().Guid)
  New-Item -Path $tmp -ItemType Directory -Force | Out-Null
  Expand-Archive -Path $item.FullName -DestinationPath $tmp -Force
  $mf=Get-ChildItem -Path $tmp -Filter manifest.json -Recurse -File | Select-Object -First 1
  if($mf){ return $mf.Directory.FullName }
 }
 throw 'manifest.json not found.'
}
function Copy-ExportRoot($Root,$Dest,$Force){
 if([string]::IsNullOrWhiteSpace($Dest)){ $Dest = ($Root.TrimEnd('\') + '_content_rehydrated') }
 if(Test-Path -LiteralPath $Dest){
  if(-not $Force){ throw "OutputPath already exists: $Dest. Use -ForceOverwrite." }
  Remove-Item -LiteralPath $Dest -Recurse -Force
 }
 Copy-Item -LiteralPath $Root -Destination $Dest -Recurse -Force
 return (Resolve-Path -LiteralPath $Dest).Path
}
function Invoke-Graph($Uri){ Invoke-MgGraphRequest -Method GET -Uri "$GraphBase/$Uri" }
function Read-Json($Path){ Get-Content -LiteralPath $Path -Raw -Encoding UTF8 | ConvertFrom-Json }
function Write-Json($Path,$Obj){ $Obj | ConvertTo-Json -Depth 100 | Set-Content -LiteralPath $Path -Encoding UTF8 }
function HasValue($v){ return ($null -ne $v -and (($v -isnot [string]) -or $v.Trim().Length -gt 0)) }

try{
 $root=Resolve-ExportRoot $SourcePath
 $target=Copy-ExportRoot -Root $root -Dest $OutputPath -Force:$ForceOverwrite
 Write-Host "Export copy: $target" -ForegroundColor Cyan
 Import-Module Microsoft.Graph.Authentication -ErrorAction Stop
 Connect-GraphForIntuneAutomation -TenantId $SourceTenantId -Scopes $Scopes
 $ctx=Get-MgContext
 Write-Host "Tenant connected: $($ctx.TenantId)" -ForegroundColor Cyan
 if($ctx.TenantId -and ($ctx.TenantId.ToLowerInvariant() -ne $SourceTenantId.ToLowerInvariant())){ throw 'Connection differs from the provided SourceTenantId.' }

 $scriptFolder=Join-Path $target '04_ScriptsPowerShell'
 if(Test-Path -LiteralPath $scriptFolder){
  foreach($file in Get-ChildItem -Path $scriptFolder -Filter '*.json' -File){
   $o=Read-Json $file.FullName; $name=$o.displayName; $id=$o.id
   try{
    $remote=Invoke-Graph -Uri "deviceManagement/deviceManagementScripts/$id"
    if((HasValue $remote.scriptContent) -and (-not (HasValue $o.scriptContent))){ $o.scriptContent=$remote.scriptContent; Write-Json $file.FullName $o; Add-Result '04_ScriptsPowerShell' $name $id 'UPDATED' 'scriptContent' '' $null }
    elseif(HasValue $o.scriptContent){ Add-Result '04_ScriptsPowerShell' $name $id 'UNCHANGED' '' 'Content already present' $null }
    else{ Add-Result '04_ScriptsPowerShell' $name $id 'MISSING' '' 'Graph does not return scriptContent' $null }
   }catch{ Add-Result '04_ScriptsPowerShell' $name $id 'ERROR' '' '' $_.Exception.Message }
  }
 }

 $remFolder=Join-Path $target '06_Remediations'
 if(Test-Path -LiteralPath $remFolder){
  foreach($file in Get-ChildItem -Path $remFolder -Filter '*.json' -File){
   $o=Read-Json $file.FullName; $name=$o.displayName; $id=$o.id
   try{
    $remote=Invoke-Graph -Uri "deviceManagement/deviceHealthScripts/$id"
    $updated=@()
    if((HasValue $remote.detectionScriptContent) -and (-not (HasValue $o.detectionScriptContent))){ $o.detectionScriptContent=$remote.detectionScriptContent; $updated += 'detectionScriptContent' }
    if((HasValue $remote.remediationScriptContent) -and (-not (HasValue $o.remediationScriptContent))){ $o.remediationScriptContent=$remote.remediationScriptContent; $updated += 'remediationScriptContent' }
    if($updated.Count -gt 0){ Write-Json $file.FullName $o; Add-Result '06_Remediations' $name $id 'UPDATED' ($updated -join ';') '' $null }
    elseif((HasValue $o.detectionScriptContent) -and (HasValue $o.remediationScriptContent)){ Add-Result '06_Remediations' $name $id 'UNCHANGED' '' 'Contents already present' $null }
    else{ Add-Result '06_Remediations' $name $id 'MISSING' '' 'Graph does not return the required contents' $null }
   }catch{ Add-Result '06_Remediations' $name $id 'ERROR' '' '' $_.Exception.Message }
  }
 }
 Save-Results
 Write-Host "Rehydrated export: $target" -ForegroundColor Green
}catch{
 Save-Results
 Write-Host "ERROR: $($_.Exception.Message)" -ForegroundColor Red
 throw
}
}