Public/Publish-IntuneApp.ps1
|
# Auto-generated by module/Build-Module.ps1 from en/scripts/Publish-IntuneApp.ps1 # Edit the source script, then re-run Build-Module.ps1. Do not edit this file directly. function Publish-IntuneApp { <# .SYNOPSIS EXPERIMENTAL: publish a Win32 app to the TARGET tenant from a local .intunewin file + metadata. ⚠️ EXPERIMENTAL — reference implementation of Microsoft's documented Win32 content-upload flow (create app → content version → file → Azure Storage SAS → block upload → commit with encryption info → set committedContentVersion). It is NOT validated against a live tenant. PREVIEW by default; add -Execute only on a SANDBOX tenant and review the result. .DESCRIPTION App binaries (.intunewin) are never returned by the export — you must provide the file. This script orchestrates the upload so the admin does not click through the portal. Metadata (the win32LobApp JSON: install/uninstall commands, detection rules, requirements) can be an exported 09_Apps profile or one drafted by the AI assistant. .PARAMETER AppMetadataJson Path to a JSON file describing the win32LobApp (must include @odata.type = #microsoft.graph.win32LobApp, displayName, publisher, installCommandLine, uninstallCommandLine, detectionRules, etc.). .PARAMETER IntuneWinFile Path to the .intunewin package (produced by the Microsoft Win32 Content Prep Tool). .PARAMETER TargetTenantId GUID of the TARGET tenant. Guardrail: refuses if the current Graph context is a different tenant. .PARAMETER Execute Perform the real upload. Without it, PREVIEW only (no write). .NOTES Requires an active Graph connection to the TARGET tenant with DeviceManagementApps.ReadWrite.All. #> [CmdletBinding()] param( [Parameter(Mandatory)][string]$AppMetadataJson, [Parameter(Mandatory)][string]$IntuneWinFile, [Parameter(Mandatory)][string]$TargetTenantId, [switch]$Execute ) $ErrorActionPreference = 'Stop' [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 $B = 'https://graph.microsoft.com/beta' foreach ($p in $AppMetadataJson, $IntuneWinFile) { if (-not (Test-Path -LiteralPath $p)) { throw "Not found: $p" } } $ctx = Get-MgContext if (-not $ctx) { throw "No Graph connection. Connect to the TARGET tenant first." } if ($ctx.TenantId -ne $TargetTenantId) { throw "GUARDRAIL: current context $($ctx.TenantId) != target $TargetTenantId." } Write-Host "" Write-Host "PUBLISH WIN32 APP (EXPERIMENTAL)" -ForegroundColor Magenta if (-not $Execute) { Write-Host "PREVIEW: no write. Add -Execute on a SANDBOX tenant to upload." -ForegroundColor Yellow } # --- 1) Read + parse the .intunewin (a zip: Metadata\Detection.xml + Contents\IntunePackage.intunewin) --- $tmp = Join-Path ([IO.Path]::GetTempPath()) ("intunewin_" + [guid]::NewGuid().Guid) Expand-Archive -LiteralPath $IntuneWinFile -DestinationPath $tmp -Force $detXml = Get-ChildItem $tmp -Recurse -Filter 'Detection.xml' | Select-Object -First 1 $encPkg = Get-ChildItem $tmp -Recurse -Filter 'IntunePackage.intunewin' | Select-Object -First 1 if (-not $detXml -or -not $encPkg) { throw 'Invalid .intunewin: Detection.xml / IntunePackage.intunewin not found.' } [xml]$det = Get-Content $detXml.FullName -Raw $ai = $det.ApplicationInfo $enc = $ai.EncryptionInfo $sizeUnenc = [int64]$ai.UnencryptedContentSize $sizeEnc = (Get-Item $encPkg.FullName).Length Write-Host (" Package: {0} | unencrypted {1:N0} B | encrypted {2:N0} B" -f $ai.Name, $sizeUnenc, $sizeEnc) -ForegroundColor Cyan $meta = Get-Content $AppMetadataJson -Raw | ConvertFrom-Json $body = [ordered]@{} foreach ($k in $meta.PSObject.Properties.Name) { if ($k -in 'id','createdDateTime','lastModifiedDateTime','uploadState','publishingState','isAssigned','committedContentVersion','size','dependentAppCount','supersedingAppCount','supersededAppCount','assignments') { continue } $body[$k] = $meta.$k } if (-not $body['@odata.type']) { $body['@odata.type'] = '#microsoft.graph.win32LobApp' } if (-not $Execute) { Write-Host (" Would create app '{0}' and upload {1:N0} bytes in blocks, then commit." -f $body['displayName'], $sizeEnc) -ForegroundColor Gray Remove-Item $tmp -Recurse -Force -ErrorAction SilentlyContinue return } function G { param($m,$u,$b) if ($b) { Invoke-MgGraphRequest -Method $m -Uri $u -Body ($b | ConvertTo-Json -Depth 40) -ContentType 'application/json' } else { Invoke-MgGraphRequest -Method $m -Uri $u } } function Wait-State { param($u,$want) for ($i=0; $i -lt 60; $i++) { $r = G GET $u; if ($r.uploadState -eq $want) { return $r }; if ($r.uploadState -like '*Failed*') { throw "uploadState=$($r.uploadState)" }; Start-Sleep 5 }; throw "timeout waiting for $want" } # --- 2) Create app, 3) content version, 4) file entry --- $app = G POST "$B/deviceAppManagement/mobileApps" $body Write-Host (" [+] app created: {0}" -f $app.id) -ForegroundColor Green $cv = G POST "$B/deviceAppManagement/mobileApps/$($app.id)/microsoft.graph.win32LobApp/contentVersions" @{} $fileBody = @{ '@odata.type'='#microsoft.graph.mobileAppContentFile'; name=$ai.FileName; size=$sizeUnenc; sizeEncrypted=$sizeEnc; isDependency=$false; manifest=$null } $file = G POST "$B/deviceAppManagement/mobileApps/$($app.id)/microsoft.graph.win32LobApp/contentVersions/$($cv.id)/files" $fileBody $fileUri = "$B/deviceAppManagement/mobileApps/$($app.id)/microsoft.graph.win32LobApp/contentVersions/$($cv.id)/files/$($file.id)" # --- 5) Wait for the Azure Storage SAS URI --- $file = Wait-State $fileUri 'azureStorageUriRequestSuccess' $sas = $file.azureStorageUri # --- 6) Block-upload the encrypted content to Azure Blob Storage --- $chunk = 6MB; $stream = [IO.File]::OpenRead($encPkg.FullName); $ids = @(); $idx = 0 try { $buf = New-Object byte[] $chunk while (($read = $stream.Read($buf,0,$chunk)) -gt 0) { $blockId = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes(('{0:D6}' -f $idx))) $ids += $blockId $data = if ($read -eq $chunk) { $buf } else { $buf[0..($read-1)] } Invoke-WebRequest -Method PUT -Uri ("{0}&comp=block&blockid={1}" -f $sas, [Uri]::EscapeDataString($blockId)) ` -Headers @{ 'x-ms-blob-type'='BlockBlob' } -Body $data -ContentType 'application/octet-stream' | Out-Null $idx++ } } finally { $stream.Dispose() } $blockList = '<?xml version="1.0" encoding="utf-8"?><BlockList>' + (($ids | ForEach-Object { "<Latest>$_</Latest>" }) -join '') + '</BlockList>' Invoke-WebRequest -Method PUT -Uri ("{0}&comp=blocklist" -f $sas) -Body $blockList -ContentType 'text/plain' | Out-Null Write-Host (" [+] uploaded {0} block(s)" -f $ids.Count) -ForegroundColor Green # --- 7) Commit with the encryption info from Detection.xml, 8) wait, 9) set committed version --- $commit = @{ fileEncryptionInfo = @{ encryptionKey = $enc.EncryptionKey macKey = $enc.MacKey initializationVector = $enc.InitializationVector mac = $enc.Mac profileIdentifier = $enc.ProfileIdentifier fileDigest = $enc.FileDigest fileDigestAlgorithm = $enc.FileDigestAlgorithm } } G POST "$fileUri/commit" $commit | Out-Null Wait-State $fileUri 'commitFileSuccess' | Out-Null G PATCH "$B/deviceAppManagement/mobileApps/$($app.id)" @{ '@odata.type'='#microsoft.graph.win32LobApp'; committedContentVersion=$cv.id } | Out-Null Remove-Item $tmp -Recurse -Force -ErrorAction SilentlyContinue Write-Host (" [OK] '{0}' published (app {1})." -f $body['displayName'], $app.id) -ForegroundColor Green } |