From b6eb4875ecf8c6cf74ebfbb857adb20ccfa33a54 Mon Sep 17 00:00:00 2001 From: Mikael Karlsson <43226266+Micke-K@users.noreply.github.com> Date: Mon, 10 Jun 2019 19:13:33 +1000 Subject: [PATCH] Add files via upload --- Extensions/AppProtection.psm1 | 304 ++++++ Extensions/Apps.psm1 | 924 +++++++++++++++++ Extensions/AutoPilot.psm1 | 238 +++++ Extensions/AzureBranding.psm1 | 310 ++++++ Extensions/AzureNative.psm1 | 169 +++ Extensions/Baseline.psm1 | 294 ++++++ Extensions/Branding.psm1 | 236 +++++ Extensions/CompliancePolicies.psm1 | 244 +++++ Extensions/ConditionalAccess.psm1 | 273 +++++ Extensions/ConfigurationItems.psm1 | 246 +++++ Extensions/EnrollmentStatusPage.psm1 | 277 +++++ Extensions/GroupPolicy.psm1 | 322 ++++++ Extensions/MDM_MAM.psm1 | 215 ++++ Extensions/MSGraphIntune.psm1 | 1438 ++++++++++++++++++++++++++ Extensions/PowerShellScripts.psm1 | 306 ++++++ Extensions/TermsAndConditions.psm1 | 243 +++++ 16 files changed, 6039 insertions(+) create mode 100644 Extensions/AppProtection.psm1 create mode 100644 Extensions/Apps.psm1 create mode 100644 Extensions/AutoPilot.psm1 create mode 100644 Extensions/AzureBranding.psm1 create mode 100644 Extensions/AzureNative.psm1 create mode 100644 Extensions/Baseline.psm1 create mode 100644 Extensions/Branding.psm1 create mode 100644 Extensions/CompliancePolicies.psm1 create mode 100644 Extensions/ConditionalAccess.psm1 create mode 100644 Extensions/ConfigurationItems.psm1 create mode 100644 Extensions/EnrollmentStatusPage.psm1 create mode 100644 Extensions/GroupPolicy.psm1 create mode 100644 Extensions/MDM_MAM.psm1 create mode 100644 Extensions/MSGraphIntune.psm1 create mode 100644 Extensions/PowerShellScripts.psm1 create mode 100644 Extensions/TermsAndConditions.psm1 diff --git a/Extensions/AppProtection.psm1 b/Extensions/AppProtection.psm1 new file mode 100644 index 0000000..10bdcf7 --- /dev/null +++ b/Extensions/AppProtection.psm1 @@ -0,0 +1,304 @@ +######################################################## +# +# Common module functions +# +######################################################## +function Add-ModuleMenuItems +{ + Add-MenuItem (New-Object PSObject -Property @{ + Title = (Get-AppProtectionName) + MenuID = "IntuneGraphAPI" + Script = [ScriptBlock]{Get-AppProtections} + }) +} + +function Get-SupportedImportObjects +{ + $global:importObjects += (New-Object PSObject -Property @{ + Selected = $true + Title = (Get-AppProtectionName) + Script = [ScriptBlock]{ + param($rootFolder) + + Write-Status "Import all app protection/configuration policies" + Import-AllAppProtectionObjects (Join-Path $rootFolder (Get-AppProtectionFolderName)) + } + }) +} + +function Get-SupportedExportObjects +{ + $global:exportObjects += (New-Object PSObject -Property @{ + Selected = $true + Title = (Get-AppProtectionName) + Script = [ScriptBlock]{ + param($rootFolder) + + Write-Status "Export all app protection/configuration policies" + Get-AppProtectionObjects | ForEach-Object { Export-SingleAppProtection $PSItem.Object (Join-Path $rootFolder (Get-AppProtectionFolderName)) } + } + }) +} + +function Export-AllObjects +{ + param($addObjectSubfolder) + + $subFolder = "" + if($addObjectSubfolder) { $subFolder = Get-AppProtectionFolderName } +} + +######################################################## +# +# Object specific functions +# +######################################################## +function Get-AppProtectionName +{ + return "App Protection/Configuration" +} + +function Get-AppProtectionFolderName +{ + return "AppProtection" +} + +function Get-AppProtections +{ + Write-Status "Loading app protections and configurations" + $dgObjects.ItemsSource = @(Get-AppProtectionObjects) + + #Scriptblocks that will perform the export tasks. empty by default + $script:exportParams = @{} + $script:exportParams.Add("ExportAllScript", [ScriptBlock]{ + Export-AllAppProtections $global:txtExportPath.Text + Set-ObjectGrid + Write-Status "" + }) + + $script:exportParams.Add("ExportSelectedScript", [ScriptBlock]{ + Export-SelectedAppProtection $global:txtExportPath.Text + Set-ObjectGrid + Write-Status "" + }) + + #Scriptblock that will perform the import all files + $script:importAll = [ScriptBlock]{ + Import-AllAppProtectionObjects $global:txtImportPath.Text + Set-ObjectGrid + } + + #Scriptblock that will perform the import of selected files + $script:importSelected = [ScriptBlock]{ + Import-AppProtectionObjects $global:lstFiles.ItemsSource -Selected + Set-ObjectGrid + } + + #Scriptblock that will read json files + $script:getImportFiles = [ScriptBlock]{ + Show-FileListBox + $global:lstFiles.ItemsSource = @(Get-JsonFileObjects $global:txtImportPath.Text -Exclude "*_Settings.json") + } + + Add-DefaultObjectButtons -export ([scriptblock]{Show-DefaultExportGrid @script:exportParams}) -import ([scriptblock]{Show-DefaultImportGrid -ImportAll $script:importAll -ImportSelected $script:importSelected -GetFiles $script:getImportFiles}) -copy ([scriptblock]{Copy-AppProtection}) +} + +function Get-AppProtectionObjects +{ + Get-GraphObjects -Url "/deviceAppManagement/managedAppPolicies" +} + +function Export-AllAppProtections +{ + param($path = "$env:Temp") + + if(-not (Test-Path $path)) { mkdir -Path $path -Force -ErrorAction SilentlyContinue | Out-Null } + + if(Test-Path $path) + { + foreach($objTmp in ($global:dgObjects.ItemsSource)) + { + Export-SingleAppProtection $objTmp.Object $path + } + } +} + +function Export-SelectedAppProtection +{ + param($path = "$env:Temp") + + Export-SingleAppProtection $global:dgObjects.SelectedItem.Object $path +} + +function Export-SingleAppProtection +{ + param($psObj, $path = "$env:Temp") + + if(-not $psObj) { return } + + if($global:runningBulkExport -ne $true) + { + if($global:chkAddCompanyName.IsChecked) { $path = Join-Path $path $global:organization.displayName } + if($global:chkAddObjectType.IsChecked) { $path = Join-Path $path (Get-AppProtectionFolderName) } + } + + if(-not (Test-Path $path)) { mkdir -Path $path -Force -ErrorAction SilentlyContinue | Out-Null } + + if(Test-Path $path) + { + Write-Status "Export $($psObj.displayName)" + + $obj = Get-AppProtectionObjectForExport $psObj + + if($obj) + { + $fileName = "$path\$((Remove-InvalidFileNameChars $obj.displayName)).json" + ConvertTo-Json $obj -Depth 5 | Out-File $fileName -Force + + Add-MigrationInfo $obj.assignments + } + $global:exportedObjects++ + } +} + +function Get-AppProtectionObjectType +{ + param($odataType) + + if($odataType -like "*targetedManagedAppConfiguration*") + { + "targetedManagedAppConfigurations" + + } + elseif($odataType -like "*iosManagedAppProtection*") + { + "iosManagedAppProtections" + } + elseif($odataType -like "*androidManagedAppProtection*") + { + "androidManagedAppProtections" + } +} + +function Get-AppProtectionObjectForExport +{ + param($obj) + + $objType = Get-AppProtectionObjectType $obj."@odata.type" + + $expand = "?`$expand=assignments" + if($objType -eq "targetedManagedAppConfigurations") + { + $expand += ",Apps" + } + + if($objType) + { + Invoke-GraphRequest -Url "/deviceAppManagement/$objType/$($obj.id)$($expand)" + } +} + +function Copy-AppProtection +{ + if(-not $dgObjects.SelectedItem) + { + [System.Windows.MessageBox]::Show("No object selected`n`nSelect app protection/configuration item you want to copy", "Error", "OK", "Error") + return + } + + $ret = Show-InputDialog "Copy app protection/configuration" "Select name for the new policy" "$($dgObjects.SelectedItem.displayName) - Copy" + + if($ret) + { + # Export profile + Write-Status "Export $($dgObjects.SelectedItem.displayName)" + $obj = Get-AppProtectionObjectForExport $dgObjects.SelectedItem.Object + if($obj) + { + # Remove assignment properties + Remove-ObjectProperty $obj "assignments" + Remove-ObjectProperty $obj "assignments@odata.context" + + # Import new profile + $obj.displayName = $ret + Import-AppProtection $obj | Out-Null + + $dgObjects.ItemsSource = @(Get-AppProtectionObjects) + } + Write-Status "" + } + $dgObjects.Focus() +} + +function Import-AppProtection +{ + param($obj) + + if(($obj | GM -MemberType NoteProperty -Name "Apps")) + { + $apps = $obj.Apps + # Remove apps properties + Remove-ObjectProperty $obj "apps" + Remove-ObjectProperty $obj "apps@odata.context" + } + + Write-Status "Import $($obj.displayName)" + + $objType = Get-AppProtectionObjectType $obj."@odata.context" + + if($objType) + { + #Import the app configuration policy + $response = Invoke-GraphRequest -Url "/deviceAppManagement/$objType" -Content (ConvertTo-Json $obj -Depth 5) -HttpMethod POST + if($response -and $apps) + { + # Import targeted apps + $response2 = Invoke-GraphRequest -Url "/deviceAppManagement/$objType/$($response.Id)/targetApps" -Content "{ apps: $(ConvertTo-Json $apps -Depth 5)}" -HttpMethod POST + } + $response + } +} + +function Import-AllAppProtectionObjects +{ + param($path = "$env:Temp") + + Import-AppProtectionObjects (Get-JsonFileObjects $path) +} + +function Import-AppProtectionObjects +{ + param( + $Objects, + + [switch] + $Selected + ) + + Write-Status "Import app protection/configuration policies" + + foreach($obj in $objects) + { + if($Selected -and $obj.Selected -ne $true) { continue } + + Write-Log "Import App Protection/Configuration: $($obj.Object.displayName)" + + $assignments = Get-GraphAssignmentsObject $obj.Object ($obj.FileInfo.DirectoryName + "\" + $obj.FileInfo.BaseName + "_assignments.json") + + $response = Import-AppProtection $obj.Object + + if($response) + { + $global:importedObjects++ + + $dataType = Get-AppProtectionObjectType $response."@odata.context" + + if($dataType) + { + Import-GraphAssignments $assignments "assignments" "/deviceAppManagement/$dataType/$($response.Id)/assign" + } + } + } + $dgObjects.ItemsSource = @(Get-AppProtectionObjects) + Write-Status "" +} \ No newline at end of file diff --git a/Extensions/Apps.psm1 b/Extensions/Apps.psm1 new file mode 100644 index 0000000..cf8a701 --- /dev/null +++ b/Extensions/Apps.psm1 @@ -0,0 +1,924 @@ +######################################################## +# +# Common module functions +# +######################################################## +function Add-ModuleMenuItems +{ + Add-MenuItem (New-Object PSObject -Property @{ + Title = (Get-ApplicationName) + MenuID = "IntuneGraphAPI" + Script = [ScriptBlock]{Get-Applications} + }) +} + +function Get-SupportedImportObjects +{ + $global:importObjects += (New-Object PSObject -Property @{ + Selected = $true + Title = (Get-ApplicationName) + Script = [ScriptBlock]{ + param($rootFolder) + + Write-Status "Import all applications" + Import-AllApplicationObjects (Join-Path $rootFolder (Get-ApplicationFolderName)) + } + }) +} + +function Get-SupportedExportObjects +{ + $global:exportObjects += (New-Object PSObject -Property @{ + Selected = $true + Title = (Get-ApplicationName) + Script = [ScriptBlock]{ + param($rootFolder) + + Write-Status "Export all applications" + Get-ApplicationObjects | ForEach-Object { Export-SingleApplication $PSItem.Object (Join-Path $rootFolder (Get-ApplicationFolderName)) } + } + }) +} + +function Export-AllObjects +{ + param($addObjectSubfolder) + + $subFolder = "" + if($addObjectSubfolder) { $subFolder = Get-ApplicationFolderName } +} + +######################################################## +# +# Object specific functions +# +######################################################## +function Get-ApplicationName +{ + (Get-ApplicationFolderName) +} + +function Get-ApplicationFolderName +{ + "Applications" +} + +function Get-Applications +{ + Write-Status "Loading applications" + $dgObjects.ItemsSource = @(Get-ApplicationObjects) + + #Scriptblocks that will perform the export tasks. empty by default + $script:exportParams = @{} + $script:exportParams.Add("ExportAllScript", [ScriptBlock]{ + Export-AllApplications $global:txtExportPath.Text + Set-ObjectGrid + Write-Status "" + }) + + $script:exportParams.Add("ExportSelectedScript", [ScriptBlock]{ + Export-SelectedApplication $global:txtExportPath.Text + Set-ObjectGrid + Write-Status "" + }) + + #Scriptblock that will perform the import all files + $script:importAll = [ScriptBlock]{ + Import-AllApplicationObjects $global:txtImportPath.Text + Set-ObjectGrid + } + + #Scriptblock that will perform the import of selected files + $script:importSelected = [ScriptBlock]{ + Import-ApplicationObjects $global:lstFiles.ItemsSource -Selected + Set-ObjectGrid + } + + #Scriptblock that will read json files + $script:getImportFiles = [ScriptBlock]{ + Show-FileListBox + $global:lstFiles.ItemsSource = @(Get-JsonFileObjects $global:txtImportPath.Text -Exclude "*_Settings.json") + } + + $importExtension = (New-Object PSObject -Property @{ + Xaml = @" + + + + + + + + + + + + + + + + + + + + + + + + +"@ + Script = [ScriptBlock]{ + param($form) + $script:txtPackagePath = $form.FindName("txtPackagePath") + $btnBrowsePackagePath = $form.FindName("btnBrowsePackagePath") + $script:txtPackagePath.Text = Get-SettingValue "IntuneAppPackages" + + $btnBrowsePackagePath.Tag = $script:txtPackagePath + $btnBrowsePackagePath.Add_Click({ + $folder = Get-Folder $this.Tag.Text + if($folder) { $this.Tag.Text = $folder } + }) + } + }) + + $script:importParams = @{} + $script:importParams.Add("Extension", $importExtension) + + Add-DefaultObjectButtons -export ([scriptblock]{Show-DefaultExportGrid @script:exportParams}) -import ([scriptblock]{Show-DefaultImportGrid -ImportAll $script:importAll -ImportSelected $script:importSelected -GetFiles $script:getImportFiles @script:importParams}) # -copy ([scriptblock]{Copy-Application}) +} + +function Get-ApplicationObjects +{ + Get-GraphObjects -Url "/deviceAppManagement/mobileApps?`$filter=(microsoft.graph.managedApp/appAvailability%20eq%20null%20or%20microsoft.graph.managedApp/appAvailability%20eq%20%27lineOfBusiness%27%20or%20isAssigned%20eq%20true)&`$orderby=displayName" +} + +function Export-AllApplications +{ + param($path = "$env:Temp") + + if(-not (Test-Path $path)) { mkdir -Path $path -Force -ErrorAction SilentlyContinue | Out-Null } + + if(Test-Path $path) + { + foreach($objTmp in ($global:dgObjects.ItemsSource)) + { + Export-SingleApplication $objTmp.Object $path + } + } +} + +function Export-SelectedApplication +{ + param($path = "$env:Temp") + + Export-SingleApplication $global:dgObjects.SelectedItem.Object $path +} + +function Export-SingleApplication +{ + param($psObj, $path = "$env:Temp") + + if(-not $psObj) { return } + + if($global:runningBulkExport -ne $true) + { + if($global:chkAddCompanyName.IsChecked) { $path = Join-Path $path $global:organization.displayName } + if($global:chkAddObjectType.IsChecked) { $path = Join-Path $path (Get-ApplicationFolderName) } + } + + if(-not (Test-Path $path)) { mkdir -Path $path -Force -ErrorAction SilentlyContinue | Out-Null } + + if(Test-Path $path) + { + Write-Status "Export $($psObj.displayName)" + $obj = Invoke-GraphRequest -Url "/deviceAppManagement/mobileApps/$($psObj.id)?`$expand=assignments" + if($obj) + { + $fileName = "$path\$((Remove-InvalidFileNameChars $obj.displayName)).json" + ConvertTo-Json $obj -Depth 5 | Out-File $fileName -Force + + Add-MigrationInfo $obj.assignments + } + $global:exportedObjects++ + } +} + +function Copy-Application +{ + if(-not $dgObjects.SelectedItem) + { + [System.Windows.MessageBox]::Show("No object selected`n`nSelect application item you want to copy", "Error", "OK", "Error") + return + } + + $ret = Show-InputDialog "Copy application" "Select name for the new object" "$($dgObjects.SelectedItem.displayName) - Copy" + + if($ret) + { + # Export profile + Write-Status "Export $($dgObjects.SelectedItem.displayName)" + # Convert to Json and back to clone the object + $obj = ConvertTo-Json $dgObjects.SelectedItem.Object -Depth 5 | ConvertFrom-Json + if($obj) + { + # Import new profile + $obj.displayName = $ret + Import-Application $obj | Out-Null + + $dgObjects.ItemsSource = @(Get-ApplicationObjects) + } + Write-Status "" + } + $dgObjects.Focus() +} + +function Import-Application +{ + param($obj) + + Remove-ObjectProperty $obj "uploadState" + Remove-ObjectProperty $obj "publishingState" + Remove-ObjectProperty $obj "isAssigned" + Remove-ObjectProperty $obj "roleScopeTagIds" + Remove-ObjectProperty $obj "dependentAppCount" + Remove-ObjectProperty $obj "committedContentVersion" + Remove-ObjectProperty $obj "id" + Remove-ObjectProperty $obj "createdDateTime" + Remove-ObjectProperty $obj "lastModifiedDateTime" + Remove-ObjectProperty $obj "isFeatured" + Remove-ObjectProperty $obj "size" + + Write-Status "Import $($obj.displayName)" + + Invoke-GraphRequest -Url "/deviceAppManagement/mobileApps" -Content (ConvertTo-Json $obj -Depth 5) -HttpMethod POST +} + +function Import-AllApplicationObjects +{ + param($path = "$env:Temp") + + Import-ApplicationObjects (Get-JsonFileObjects $path) +} + +function Import-ApplicationObjects +{ + param( + $Objects, + + [switch] + $Selected + ) + + Write-Status "Import applications" + + foreach($obj in $objects) + { + if($Selected -and $obj.Selected -ne $true) { continue } + + if($global:runningBulkImport) + { + $pkgPath = Get-SettingValue "IntuneAppPackages" + } + else + { + $pkgPath = $script:txtPackagePath.Text + } + $appFile = "$($pkgPath)\$($obj.Object.fileName)" + + if(Test-Path $appFile) + { + Write-Log "Import Application: $($obj.Object.displayName) ($($obj.Object."@odata.type"))" + + $assignments = Get-GraphAssignmentsObject $obj.Object ($obj.FileInfo.DirectoryName + "\" + $obj.FileInfo.BaseName + "_assignments.json") + $response = Import-Application $obj.Object + + if($response) + { + $global:importedObjects++ + Copy-AppPackageToIntune $appFile $response + + Import-GraphAssignments $assignments "mobileAppAssignments" "/deviceAppManagement/mobileApps/$($response.Id)/assign" "#microsoft.graph.mobileAppAssignment" + } + } + else + { + Write-Log "Application file $appFile not found. Skipping app $($obj.Object.displayName)" 3 + } + } + $dgObjects.ItemsSource = @(Get-ApplicationObjects) + Write-Status "" +} + +function Start-DownloadAppContent +{ + param($obj, $path) + # Not use but kept for reference. File can be download but it will be encrypted + + $appId = $obj.Id + + $appId = "b2b79110-31f7-40bd-923b-228415c92cdb" + + $appInfo = Invoke-GraphRequest -Url "$($global:graphURL)/deviceAppManagement/mobileApps/$appId" + + $appType = $appInfo.'@odata.type'.Trim('#') + + $contentVersions = Invoke-GraphRequest -Url "$($global:graphURL)/deviceAppManagement/mobileApps/$appId/$appType/contentVersions" + + $contentVerId = $contentVersions.Value[0].id + + $contentFiles = Invoke-GraphRequest "$($global:graphURL)/deviceAppManagement/mobileApps/$appId/$appType/contentVersions/$contentVerId/files" + + foreach($tmpFile in $contentFiles) + { + $contentFile = Invoke-GraphRequest -Url "$($global:graphURL)/deviceAppManagement/mobileApps/$appId/$appType/contentVersions/$contentVerId/files/$($tmpFile.Id)" + $downloadUrl = $contentFile.azureStorageUri + } +} + +function Copy-AppPackageToIntune +{ + param($packageFile, $appObj) + + $appType = $appObj.'@odata.type'.Trim('#') + + if($appType -eq "microsoft.graph.win32LobApp") + { + Copy-Win32LOBPackage $packageFile $appObj + } + elseif($appType -eq "microsoft.graph.windowsMobileMSI") + { + Copy-MSILOB $packageFile $appObj + } + elseif($appType -eq "microsoft.graph.iosLOBApp") + { + Copy-iOSLOB $packageFile $appObj + } + elseif($appType -eq "microsoft.graph.androidLOBApp") + { + Copy-AndroidLOB $packageFile $appObj + } +} + +######################################################################################### +# +# Upload file functions are based on the following scripts +# https://github.com/microsoftgraph/powershell-intune-samples/tree/master/LOB_Application +# +######################################################################################### + +function Export-IntunewinFileObject +{ + param($intunewinFile, $objectName, $toFile) + + Add-Type -Assembly System.IO.Compression.FileSystem + + $zip = [IO.Compression.ZipFile]::OpenRead($intunewinFile) + + $zip.Entries | where { $_.Name -like $objectName } | foreach { + + [System.IO.Compression.ZipFileExtensions]::ExtractToFile($_, $toFile, $true) + } + + $zip.Dispose() +} + +function Get-MSIFileInformation +{ + param($MSIFile, $Properties) + + $values = @{} + + try + { + $wiObj = New-Object -ComObject WindowsInstaller.Installer + $MSIDb = $wiObj.GetType().InvokeMember("OpenDatabase", "InvokeMethod", $null, $wiObj, @($MSIFile, 0)) + + foreach($prop in $Properties) + { + $Query = "SELECT Value FROM Property WHERE Property = '$($prop)'" + $View = $MSIDb.GetType().InvokeMember("OpenView", "InvokeMethod", $null, $MSIDb, ($Query)) + $View.GetType().InvokeMember("Execute", "InvokeMethod", $null, $View, $null) | Out-Null + $Record = $View.GetType().InvokeMember("Fetch", "InvokeMethod", $null, $View, $null) + $values.Add($prop, $Record.GetType().InvokeMember("StringData", "GetProperty", $null, $Record, 1).ToString().Trim()) + } + + $MSIDb.GetType().InvokeMember("Commit", "InvokeMethod", $null, $MSIDb, $null) | Out-Null + $View.GetType().InvokeMember("Close", "InvokeMethod", $null, $View, $null) | Out-Null + $MSIDb = $null + $View = $null + } + catch + { + Write-Log "Failed to get MSI info from $MSIFile. $($_.Exception.Message)" 3 + } + finally + { + [System.Runtime.Interopservices.Marshal]::ReleaseComObject($wiObj) | Out-Null + [System.GC]::Collect() | Out-Null + } + + $values +} + +function Copy-MSILOB +{ + param($msiFile, $appObj) + + if(-not $msiFile -or (Test-Path $msiFile) -eq $false) + { + return + } + + $appId = $appObj.Id + $appType = $appObj.'@odata.type'.Trim('#') + + $tmpFile = [IO.Path]::GetTempFileName() + + $msiInfo = Get-MSIFileInformation $msiFile @("ProductName", "ProductCode", "ProductVersion", "ProductLanguage") + + if(-not $msiInfo) { return } + + $fileEncryptionInfo = New-IntuneEncryptedFile $msiFile $tmpFile + + [xml]$manifestXML = '' + $manifestXML.MobileMsiData.MsiUpgradeCode = $msiInfo["ProductCode"] + + $appFileBody = @{ + "@odata.type" = "#microsoft.graph.mobileAppContentFile" + name = [IO.Path]::GetFileName($msiFile) + size = (Get-Item $msiFile).Length + sizeEncrypted = (Get-Item $tmpFile).Length + manifest = [Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes($manifestXML.OuterXml)) + } + + Add-FileToIntuneApp $appId $appType $tmpFile $appFileBody + + Remove-Item $tmpFile -Force +} + +function Copy-iOSLOB +{ + param($pkgFile, $appObj) + + if(-not $pkgFile -or (Test-Path $pkgFile) -eq $false) + { + return + } + + $appId = $appObj.Id + $appType = $appObj.'@odata.type'.Trim('#') + + $tmpFile = [IO.Path]::GetTempFileName() + + $fileEncryptionInfo = New-IntuneEncryptedFile $pkgFile $tmpFile + + [string]$manifestStr = 'itemsassetskindsoftware-packageurl{UrlPlaceHolder}metadataAppRestrictionPolicyTemplate http://management.microsoft.com/PolicyTemplates/AppRestrictions/iOS/v1AppRestrictionTechnologyWindows Intune Application Restrictions Technology for iOSIntuneMAMVersionCFBundleSupportedPlatformsiPhoneOSMinimumOSVersion9.0bundle-identifierbundleidbundle-versionbundleversionkindsoftwaresubtitleLaunchMeSubtitletitlebundletitle' + + $manifestStr = $manifestStr.replace("bundleid", $appObj.bundleId) + $manifestStr = $manifestStr.replace("bundleversion",$appObj.identityVersion) + $manifestStr = $manifestStr.replace("bundletitle",$appObj.$displayName) + + $appFileBody = @{ + "@odata.type" = "#microsoft.graph.mobileAppContentFile" + name = [IO.Path]::GetFileName($pkgFile) + size = (Get-Item $pkgFile).Length + sizeEncrypted = (Get-Item $tmpFile).Length + manifest = [Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes($manifestStr)) + } + + Add-FileToIntuneApp $appId $appType $tmpFile $appFileBody + + Remove-Item $tmpFile -Force +} + +function Copy-AndroidLOB +{ + param($pkgFile, $appObj) + + if(-not $pkgFile -or (Test-Path $pkgFile) -eq $false) + { + return + } + + $appId = $appObj.Id + $appType = $appObj.'@odata.type'.Trim('#') + + $tmpFile = [IO.Path]::GetTempFileName() + + $fileEncryptionInfo = New-IntuneEncryptedFile $pkgFile $tmpFile + + [xml]$manifestXML = 'com.leadapps.android.radio.ncp101.0.5.4A_Online_Radio_1.0.5.4.apk3' + + $manifestXML.AndroidManifestProperties.Package = $appObj.identityName + $manifestXML.AndroidManifestProperties.PackageVersionCode = $appObj.versionCode + $manifestXML.AndroidManifestProperties.PackageVersionName = $appObj.versionName + $manifestXML.AndroidManifestProperties.ApplicationName = [IO.Path]::GetFileName($pkgFile) + + $appFileBody = @{ + "@odata.type" = "#microsoft.graph.mobileAppContentFile" + name = [IO.Path]::GetFileName($pkgFile) + size = (Get-Item $pkgFile).Length + sizeEncrypted = (Get-Item $tmpFile).Length + manifest = [Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes($manifestXML.OuterXml)) + } + + Add-FileToIntuneApp $appId $appType $tmpFile $appFileBody + + Remove-Item $tmpFile -Force +} + +function Copy-Win32LOBPackage +{ + param($intunewinFile, $appObj) + + if(-not $intunewinFile -or (Test-Path $intunewinFile) -eq $false) + { + return + } + + $appId = $appObj.Id + $appType = $appObj.'@odata.type'.Trim('#') + + #Extract the detection.xml from the intunewin file + + $tmpFile = [IO.Path]::GetTempFileName() + + Export-IntunewinFileObject $intunewinFile "detection.xml" $tmpFile + + [xml]$DetectionXML = Get-Content $tmpFile + + Remove-Item -Path $tmpFile + + # Get encryption info from detection.xml and build encryptionInfo object + + $encryptionInfo = @{} + $encryptionInfo.encryptionKey = $DetectionXML.ApplicationInfo.EncryptionInfo.EncryptionKey + $encryptionInfo.macKey = $DetectionXML.ApplicationInfo.EncryptionInfo.macKey + $encryptionInfo.initializationVector = $DetectionXML.ApplicationInfo.EncryptionInfo.initializationVector + $encryptionInfo.mac = $DetectionXML.ApplicationInfo.EncryptionInfo.mac + $encryptionInfo.profileIdentifier = "ProfileVersion1" + $encryptionInfo.fileDigest = $DetectionXML.ApplicationInfo.EncryptionInfo.fileDigest + $encryptionInfo.fileDigestAlgorithm = $DetectionXML.ApplicationInfo.EncryptionInfo.fileDigestAlgorithm + + $tmpIntunewinPath = ([IO.Path]::GetTempPath() + [Guid]::NewGuid().ToString("n")) + mkdir $tmpIntunewinPath | Out-Null + $tmpIntunewinFile = $tmpIntunewinPath + "\" + $DetectionXML.ApplicationInfo.FileName + + # Extract the encrypted file from the intunewin file + Export-IntunewinFileObject $intunewinFile $DetectionXML.ApplicationInfo.FileName $tmpIntunewinFile + + # Create mobileAppContentFile object for the file + $fileEncryptionInfo = @{} + $fileEncryptionInfo.fileEncryptionInfo = $encryptionInfo + + $fileBody = @{ + "@odata.type" = "#microsoft.graph.mobileAppContentFile" + name = $DetectionXML.ApplicationInfo.FileName + size = [int64]$DetectionXML.ApplicationInfo.UnencryptedContentSize + sizeEncrypted = (Get-Item $tmpIntunewinFile).Length + manifest = $null + isDependency = $false + } + + Add-FileToIntuneApp $appId $appType $tmpIntunewinFile $fileBody + + # Remove extracted inintunewin file + Remove-Item $tmpIntunewinPath -Force -Recurse +} + +function Add-FileToIntuneApp +{ + param($appId, $appType, $appFile, $fileBody) + + $contentVersion = Invoke-GraphRequest -Url "/deviceAppManagement/mobileApps/$appId/$appType/contentVersions" + $contentVersionId = $contentVersion.value[0].id + $fileObj = Invoke-GraphRequest -Url "/deviceAppManagement/mobileApps/$appId/$appType/contentVersions/$contentVersionId/files" -HttpMethod POST -Content (ConvertTo-Json $fileBody -Depth 5) + + if(-not $fileObj) + { + return + } + + # Wait for Azure storage URI + $fileObj = Wait-IntuneFileState "/deviceAppManagement/mobileApps/$appId/$appType/contentVersions/$contentVersionId/files/$($fileObj.Id)" "AzureStorageUriRequest" + if(-not $fileObj) + { + return + } + + # Upload file + Send-IntuneFileToAzureStorage $fileObj.azureStorageUri $appFile "/deviceAppManagement/mobileApps/$appId/$appType/contentVersions/$contentVersionId/files/$($fileObj.Id)" + + # Commit the file + $reponse = Invoke-GraphRequest -Url "/deviceAppManagement/mobileApps/$appId/$appType/contentVersions/$contentVersionId/files/$($fileObj.Id)/commit" -HttpMethod POST -Content (ConvertTo-Json $fileEncryptionInfo -Depth 5) + + Wait-IntuneFileState "/deviceAppManagement/mobileApps/$appId/$appType/contentVersions/$contentVersionId/files/$($fileObj.Id)" "CommitFile" + + # Commit the content version + $commitAppBody = @{ + "@odata.type" = "#$appType" + committedContentVersion = $contentVersionId + } + + $reponse = Invoke-GraphRequest -Url "/deviceAppManagement/mobileApps/$appId" -HttpMethod PATCH -Content (ConvertTo-Json $commitAppBody -Depth 5) +} + +function Wait-IntuneFileState +{ + param($fileUri, $state, $maxWait = 60) + + Write-Status "Wait for state $state" + + $endWait = (Get-Date).AddMinutes($maxWait) + + $successState = "$($state)Success" + $pendingState = "$($state)Pending" + $failedState = "$($state)Failed" + $timedOutState = "$($state)TimedOut" + + $file = $null + $succes = $false + + while ((Get-Date) -lt $endWait) + { + $file = Invoke-GraphRequest -Url $fileUri + + if ($file.uploadState -eq $successState) + { + $succes = $true + break + } + elseif ($file.uploadState -ne $pendingState) + { + Write-Log "Failed to upload file. State: $($file.uploadState)" 3 + return + } + + Start-Sleep -s 5 + } + + if($succes -eq $false) + { + Write-Log "Wait for state operation timed out" 3 + return + } + + $file +} + +function Send-IntuneFileToAzureStorage +{ + param($sasUri, $filepath, $fileUri) + + try + { + $chunkSizeInBytes = 5MB + + # Start the timer for SAS URI renewal. + $sasRenewalTimer = [System.Diagnostics.Stopwatch]::StartNew() + + # Find the file size and open the file. + $fileSize = (Get-Item $filepath).length + $chunks = [Math]::Ceiling($fileSize / $chunkSizeInBytes) + $reader = New-Object System.IO.BinaryReader([System.IO.File]::Open($filepath, [System.IO.FileMode]::Open)) + $position = $reader.BaseStream.Seek(0, [System.IO.SeekOrigin]::Begin) + + # Upload each chunk. Check whether a SAS URI renewal is required after each chunk is uploaded and renew if needed. + $ids = @() + + for ($chunk = 0; $chunk -lt $chunks; $chunk++) + { + + $id = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes($chunk.ToString("0000"))) + $ids += $id + + $start = $chunk * $chunkSizeInBytes + $length = [Math]::Min($chunkSizeInBytes, $fileSize - $start) + $bytes = $reader.ReadBytes($length) + + $currentChunk = $chunk + 1 + + Write-Status "Uploading file to Azure Storage`n`nUploading chunk $currentChunk of $chunks ($(($currentChunk / $chunks*100))%)" + + Write-AzureStorageChunk $sasUri $id $bytes + + if ($currentChunk -lt $chunks -and $sasRenewalTimer.ElapsedMilliseconds -ge 450000) + { + Request-RenewAzureStorageUpload $fileUri + $sasRenewalTimer.Restart() + } + } + $reader.Close() + } + finally + { + if ($reader -ne $null) { $reader.Dispose() } + } + + # Finalize the upload. + $uploadResponse = Set-FinalizeAzureStorageUpload $sasUri $ids +} + +function Request-RenewAzureStorageUpload +{ + param($fileUri) + + $fileObj = Invoke-GraphRequest -Url "$fileUri/renewUpload" -HttpMethod POST + + $file = Wait-IntuneFileState $fileUri "AzureStorageUriRenewal" $azureStorageRenewSasUriBackOffTimeInSeconds +} + +function Set-FinalizeAzureStorageUpload +{ + param($sasUri, $ids) + + $uri = "$sasUri&comp=blocklist" + + if(($uri -notmatch "^http://|^https://")) + { + $uri = $global:graphURL + "/" + $uri.TrimStart('/') + } + + $request = "PUT $uri" + + $xml = '' + foreach ($id in $ids) + { + $xml += "$id" + } + $xml += '' + + try + { + Invoke-RestMethod $uri -Method Put -Body $xml + } + catch + { + Write-Log "Failed to finilize upload. $($_.Exception.Message)" 3 + } +} + +function Write-AzureStorageChunk +{ + param($sasUri, $id, $body) + + $uri = "$sasUri&comp=block&blockid=$id" + + if(($uri -notmatch "^http://|^https://")) + { + $uri = $global:graphURL + "/" + $uri.TrimStart('/') + } + + $request = "PUT $uri" + + $iso = [System.Text.Encoding]::GetEncoding("iso-8859-1") + $encodedBody = $iso.GetString($body) + $headers = @{ + "x-ms-blob-type" = "BlockBlob" + } + + try + { + $response = Invoke-WebRequest $uri -Method Put -Headers $headers -Body $encodedBody + } + catch + { + Write-Log "Failed to upload file chunk. $($_.Exception.Message)" 3 + } +} + +function Get-IntuneKey +{ + try + { + $aes = [System.Security.Cryptography.Aes]::Create() + $aesProvider = New-Object System.Security.Cryptography.AesCryptoServiceProvider + $aesProvider.GenerateKey() + $aesProvider.Key + } + finally + { + if ($aesProvider -ne $null) { $aesProvider.Dispose() } + if ($aes -ne $null) { $aes.Dispose() } + } +} + +function Get-IntuneKeyIV +{ + + try + { + $aes = [System.Security.Cryptography.Aes]::Create() + $aes.IV + } + finally + { + if ($aes -ne $null) { $aes.Dispose() } + } +} + +function Start-EncryptFileWithIV +{ + param($sourceFile, $targetFile, $encryptionKey, $hmacKey, $initializationVector) + + $bufferBlockSize = 1024 * 4 + $computedMac = $null + + try + { + $aes = [System.Security.Cryptography.Aes]::Create() + $hmacSha256 = New-Object System.Security.Cryptography.HMACSHA256 + $hmacSha256.Key = $hmacKey + $hmacLength = $hmacSha256.HashSize / 8 + + $buffer = New-Object byte[] $bufferBlockSize + $bytesRead = 0 + + $targetStream = [System.IO.File]::Open($targetFile, [System.IO.FileMode]::Create, [System.IO.FileAccess]::Write, [System.IO.FileShare]::Read) + $targetStream.Write($buffer, 0, $hmacLength + $initializationVector.Length) + + try + { + $encryptor = $aes.CreateEncryptor($encryptionKey, $initializationVector) + $sourceStream = [System.IO.File]::Open($sourceFile, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::Read) + $cryptoStream = New-Object System.Security.Cryptography.CryptoStream -ArgumentList @($targetStream, $encryptor, [System.Security.Cryptography.CryptoStreamMode]::Write) + + $targetStream = $null + while (($bytesRead = $sourceStream.Read($buffer, 0, $bufferBlockSize)) -gt 0) + { + $cryptoStream.Write($buffer, 0, $bytesRead) + $cryptoStream.Flush() + } + $cryptoStream.FlushFinalBlock() + } + finally + { + if ($cryptoStream -ne $null) { $cryptoStream.Dispose() } + if ($sourceStream -ne $null) { $sourceStream.Dispose() } + if ($encryptor -ne $null) { $encryptor.Dispose() } + } + + try + { + $finalStream = [System.IO.File]::Open($targetFile, [System.IO.FileMode]::Open, [System.IO.FileAccess]::ReadWrite, [System.IO.FileShare]::Read) + + $finalStream.Seek($hmacLength, [System.IO.SeekOrigin]::Begin) > $null + $finalStream.Write($initializationVector, 0, $initializationVector.Length) + $finalStream.Seek($hmacLength, [System.IO.SeekOrigin]::Begin) > $null + + $hmac = $hmacSha256.ComputeHash($finalStream) + $computedMac = $hmac + + $finalStream.Seek(0, [System.IO.SeekOrigin]::Begin) > $null + $finalStream.Write($hmac, 0, $hmac.Length) + } + finally + { + if ($finalStream -ne $null) { $finalStream.Dispose() } + } + } + finally + { + if ($targetStream -ne $null) { $targetStream.Dispose() } + if ($aes -ne $null) { $aes.Dispose() } + } + + $computedMac +} + +function New-IntuneEncryptedFile +{ + param($sourceFile, $targetFile) + + $encryptionKey = Get-IntuneKey + $hmacKey = Get-IntuneKey + $initializationVector = Get-IntuneKeyIV + + # Create the encrypted target file and compute the HMAC value. + $mac = Start-EncryptFileWithIV $sourceFile $targetFile $encryptionKey $hmacKey $initializationVector + + # Compute the SHA256 hash of the source file and convert the result to bytes. + $fileDigest = (Get-FileHash $sourceFile -Algorithm SHA256).Hash + $fileDigestBytes = New-Object byte[] ($fileDigest.Length / 2) + for ($i = 0; $i -lt $fileDigest.Length; $i += 2) + { + $fileDigestBytes[$i / 2] = [System.Convert]::ToByte($fileDigest.Substring($i, 2), 16) + } + + # Return an object that will serialize correctly to the file commit Graph API. + $encryptionInfo = @{} + $encryptionInfo.encryptionKey = [System.Convert]::ToBase64String($encryptionKey) + $encryptionInfo.macKey = [System.Convert]::ToBase64String($hmacKey) + $encryptionInfo.initializationVector = [System.Convert]::ToBase64String($initializationVector) + $encryptionInfo.mac = [System.Convert]::ToBase64String($mac) + $encryptionInfo.profileIdentifier = "ProfileVersion1" + $encryptionInfo.fileDigest = [System.Convert]::ToBase64String($fileDigestBytes) + $encryptionInfo.fileDigestAlgorithm = "SHA256" + + $fileEncryptionInfo = @{} + $fileEncryptionInfo.fileEncryptionInfo = $encryptionInfo + + $fileEncryptionInfo +} \ No newline at end of file diff --git a/Extensions/AutoPilot.psm1 b/Extensions/AutoPilot.psm1 new file mode 100644 index 0000000..a0dbfb5 --- /dev/null +++ b/Extensions/AutoPilot.psm1 @@ -0,0 +1,238 @@ +######################################################## +# +# Common module functions +# +######################################################## +function Add-ModuleMenuItems +{ + Add-MenuItem (New-Object PSObject -Property @{ + Title = (Get-AutoPilotName) + MenuID = "IntuneGraphAPI" + Script = [ScriptBlock]{Get-AutoPilots} + }) +} + +function Get-SupportedImportObjects +{ + $global:importObjects += (New-Object PSObject -Property @{ + Selected = $true + Title = (Get-AutoPilotName) + Script = [ScriptBlock]{ + param($rootFolder) + + Write-Status "Import all AutoPilot policies" + Import-AllAutoPilotObjects (Join-Path $rootFolder (Get-AutoPilotFolderName)) + } + }) +} + +function Get-SupportedExportObjects +{ + $global:exportObjects += (New-Object PSObject -Property @{ + Selected = $true + Title = (Get-AutoPilotName) + Script = [ScriptBlock]{ + param($rootFolder) + + Write-Status "Export all AutoPilot policies" + Get-AutoPilotObjects | ForEach-Object { Export-SingleAutoPilotObject $PSItem.Object (Join-Path $rootFolder (Get-AutoPilotFolderName)) } + } + }) +} + +function Export-AllObjects +{ + param($addObjectSubfolder) + + $subFolder = "" + if($addObjectSubfolder) { $subFolder = Get-AutoPilotFolderName } +} + +######################################################## +# +# Object specific functions +# +######################################################## +function Get-AutoPilotName +{ + (Get-AutoPilotFolderName) +} + +function Get-AutoPilotFolderName +{ + "AutoPilot" +} + +function Get-AutoPilots +{ + Write-Status "Loading AutoPilot profiles" + $dgObjects.ItemsSource = @(Get-AutoPilotObjects) + + #Scriptblocks that will perform the export tasks. empty by default + $script:exportParams = @{} + $script:exportParams.Add("ExportAllScript", [ScriptBlock]{ + Export-AllAutoPilots $global:txtExportPath.Text + Set-ObjectGrid + Write-Status "" + }) + + $script:exportParams.Add("ExportSelectedScript", [ScriptBlock]{ + Export-SelectedAutoPilotObject $global:txtExportPath.Text + Set-ObjectGrid + Write-Status "" + }) + + #Scriptblock that will perform the import all files + $script:importAll = [ScriptBlock]{ + Import-AllAutoPilotObjects $global:txtImportPath.Text + Set-ObjectGrid + } + + #Scriptblock that will perform the import of selected files + $script:importSelected = [ScriptBlock]{ + Import-AutoPilotObjects $global:lstFiles.ItemsSource -Selected + Set-ObjectGrid + } + + #Scriptblock that will read json files + $script:getImportFiles = [ScriptBlock]{ + Show-FileListBox + $global:lstFiles.ItemsSource = @(Get-JsonFileObjects $global:txtImportPath.Text -Exclude "*_Settings.json") + } + + Add-DefaultObjectButtons -export ([scriptblock]{Show-DefaultExportGrid @script:exportParams}) -import ([scriptblock]{Show-DefaultImportGrid -ImportAll $script:importAll -ImportSelected $script:importSelected -GetFiles $script:getImportFiles}) -copy ([scriptblock]{Copy-AutoPilot}) +} + +function Get-AutoPilotObjects +{ + Get-GraphObjects -Url "/deviceManagement/windowsAutopilotDeploymentProfiles" +} + +function Export-AllAutoPilots +{ + param($path = "$env:Temp") + + if(-not (Test-Path $path)) { mkdir -Path $path -Force -ErrorAction SilentlyContinue | Out-Null } + + if(Test-Path $path) + { + foreach($objTmp in ($global:dgObjects.ItemsSource)) + { + Export-SingleAutoPilotObject $objTmp.Object $path + } + } +} + +function Export-SelectedAutoPilotObject +{ + param($path = "$env:Temp") + + Export-SingleAutoPilotObject $global:dgObjects.SelectedItem.Object $path +} + +function Export-SingleAutoPilotObject +{ + param($psObj, $path = "$env:Temp") + + if(-not $psObj) { return } + + if($global:runningBulkExport -ne $true) + { + if($global:chkAddCompanyName.IsChecked) { $path = Join-Path $path $global:organization.displayName } + if($global:chkAddObjectType.IsChecked) { $path = Join-Path $path (Get-AutoPilotFolderName) } + } + + if(-not (Test-Path $path)) { mkdir -Path $path -Force -ErrorAction SilentlyContinue | Out-Null } + + if(Test-Path $path) + { + Write-Status "Export $($psObj.displayName)" + $obj = Invoke-GraphRequest -Url "/deviceManagement/windowsAutopilotDeploymentProfiles/$($psObj.id)?`$expand=assignments" + if($obj) + { + $fileName = "$path\$((Remove-InvalidFileNameChars $obj.displayName)).json" + ConvertTo-Json $obj -Depth 5 | Out-File $fileName -Force + + Add-MigrationInfo $obj.assignments + } + $global:exportedObjects++ + } +} + +function Copy-AutoPilot +{ + if(-not $dgObjects.SelectedItem) + { + [System.Windows.MessageBox]::Show("No object selected`n`nSelect AutoPilot item you want to copy", "Error", "OK", "Error") + return + } + + $ret = Show-InputDialog "Copy AutoPilot" "Select name for the new object" "$($dgObjects.SelectedItem.displayName) Copy" + + if($ret) + { + # Export profile + Write-Status "Export $($dgObjects.SelectedItem.displayName)" + # Convert to Json and back to clone the object + $obj = ConvertTo-Json $dgObjects.SelectedItem.Object -Depth 5 | ConvertFrom-Json + if($obj) + { + # Import new profile + $obj.displayName = Remove-InvalidFileNameChars $ret + Import-AutoPilot $obj | Out-Null + + $dgObjects.ItemsSource = @(Get-AutoPilotObjects) + } + Write-Status "" + } + $dgObjects.Focus() +} + +function Import-AutoPilot +{ + param($obj) + + Write-Status "Import $($obj.displayName)" + + Invoke-GraphRequest -Url "/deviceManagement/windowsAutopilotDeploymentProfiles" -Content (ConvertTo-Json $obj -Depth 5) -HttpMethod POST +} + +function Import-AllAutoPilotObjects +{ + param( + $path = "$env:Temp" + ) + + Import-AutoPilotObjects (Get-JsonFileObjects $path) +} + +function Import-AutoPilotObjects +{ + param( + $Objects, + + [switch] + $Selected + ) + + Write-Status "Import AutoPilot profiles" + + foreach($obj in $objects) + { + if($Selected -and $obj.Selected -ne $true) { continue } + + Write-Log "Import AutoPilot profile: $($obj.Object.displayName)" + + $assignments = Get-GraphAssignmentsObject $obj.Object ($obj.FileInfo.DirectoryName + "\" + $obj.FileInfo.BaseName + "_assignments.json") + + $response = Import-AutoPilot $obj.Object + + if($response) + { + $global:importedObjects++ + Import-GraphAssignments2 $assignments "/deviceManagement/windowsAutopilotDeploymentProfiles/$($response.Id)/assignments" + } + } + $dgObjects.ItemsSource = @(Get-AutoPilotObjects) + Write-Status "" +} \ No newline at end of file diff --git a/Extensions/AzureBranding.psm1 b/Extensions/AzureBranding.psm1 new file mode 100644 index 0000000..c92f8b8 --- /dev/null +++ b/Extensions/AzureBranding.psm1 @@ -0,0 +1,310 @@ +######################################################## +# +# Common module functions +# +######################################################## +function Add-ModuleMenuItems +{ + Add-MenuItem (New-Object PSObject -Property @{ + Title = (Get-AZBrandingName) + MenuID = "IntuneGraphAPI" + Script = [ScriptBlock]{Get-AZBrandings} + }) +} + +function Get-SupportedImportObjects +{ + $global:importObjects += (New-Object PSObject -Property @{ + Selected = $true + Title = (Get-AZBrandingName) + Script = [ScriptBlock]{ + param($rootFolder) + + Write-Status "Import all Azure branding" + Import-AllAZBrandingObjects (Join-Path $rootFolder (Get-AZBrandingFolderName)) + } + }) +} + +function Get-SupportedExportObjects +{ + $global:exportObjects += (New-Object PSObject -Property @{ + Selected = $true + Title = (Get-AZBrandingName) + Script = [ScriptBlock]{ + param($rootFolder) + + Write-Status "Export all Azure branding" + Get-AZBrandingObjects | ForEach-Object { Export-SingleAZBranding $PSItem.Object (Join-Path $rootFolder (Get-AZBrandingFolderName)) } + } + }) +} + +function Export-AllObjects +{ + param($addObjectSubfolder) + + $subFolder = "" + if($addObjectSubfolder) { $subFolder = Get-AZBrandingFolderName } +} + +######################################################## +# +# Object specific functions +# +######################################################## +function Get-AZBrandingName +{ + return "Azure Branding" +} + +function Get-AZBrandingFolderName +{ + return "AZBranding" +} + +function Get-AZBrandings +{ + Write-Status "Loading Azure brandings" + $dgObjects.ItemsSource = @(Get-AZBrandingObjects) + + #Scriptblocks that will perform the export tasks. empty by default + $script:exportParams = @{} + $script:exportParams.Add("ExportAllScript", [ScriptBlock]{ + Export-AllAZBrandings $global:txtExportPath.Text + Set-ObjectGrid + Write-Status "" + }) + + $script:exportParams.Add("ExportSelectedScript", [ScriptBlock]{ + Export-SelectedAZBranding $global:txtExportPath.Text + Set-ObjectGrid + Write-Status "" + }) + + $script:exportParams.Add("DisplayColumn", "localeDisplayName") + + #Scriptblock that will perform the import all files + $script:importAll = [ScriptBlock]{ + Import-AllAZBrandingObjects $global:txtImportPath.Text + Set-ObjectGrid + } + + #Scriptblock that will perform the import of selected files + $script:importSelected = [ScriptBlock]{ + Import-AZBrandingObjects $global:lstFiles.ItemsSource -Selected + Set-ObjectGrid + } + + #Scriptblock that will read json files + $script:getImportFiles = [ScriptBlock]{ + Show-FileListBox + $global:lstFiles.ItemsSource = @(Get-JsonFileObjects $global:txtImportPath.Text -Exclude "*_Settings.json") + } + + Add-DefaultObjectButtons -export ([scriptblock]{Show-DefaultExportGrid @script:exportParams}) -import ([scriptblock]{Show-DefaultImportGrid -ImportAll $script:importAll -ImportSelected $script:importSelected -GetFiles $script:getImportFiles}) +} + +function Get-AZBrandingObjects +{ + $response = Get-AzureNativeObjects "LoginTenantBrandings" -property @('locale', 'localeDisplayName') + if($response) + { + $response | Where { $_.Object.isConfigured -eq $true } + } +} + +function Export-AllAZBrandings +{ + param($path = "$env:Temp") + + if(-not (Test-Path $path)) { mkdir -Path $path -Force -ErrorAction SilentlyContinue | Out-Null } + + if(Test-Path $path) + { + foreach($objTmp in ($global:dgObjects.ItemsSource)) + { + Export-SingleAZBranding $objTmp.Object $path + } + } +} + +function Export-SelectedAZBranding +{ + param($path = "$env:Temp") + + Export-SingleAZBranding $global:dgObjects.SelectedItem.Object $path +} + +function Export-SingleAZBranding +{ + param($psObj, $path = "$env:Temp") + + if(-not $psObj) { return } + + if($global:runningBulkExport -ne $true) + { + if($global:chkAddCompanyName.IsChecked) { $path = Join-Path $path $global:organization.displayName } + if($global:chkAddObjectType.IsChecked) { $path = Join-Path $path (Get-AZBrandingFolderName) } + } + + if(-not (Test-Path $path)) { mkdir -Path $path -Force -ErrorAction SilentlyContinue | Out-Null } + + if(Test-Path $path) + { + Write-Status "Export $($psObj.localeDisplayName)" + $obj = Invoke-AzureNativeRequest "LoginTenantBrandings/$($psObj.locale)" + if($obj) + { + $fileName = "$path\$((Remove-InvalidFileNameChars $obj.localeDisplayName)).json" + ConvertTo-Json $obj -Depth 5 | Out-File $fileName -Force + + Save-AzureBrandingFile $obj "tileLogoUrl" $path + Save-AzureBrandingFile $obj "bannerLogoUrl" $path + Save-AzureBrandingFile $obj "illustrationUrl" $path + Save-AzureBrandingFile $obj "squareLogoDarkUrl" $path + + $global:exportedObjects++ + } + Set-ObjectPath $global:txtExportPath.Text + } +} + +function Save-AzureBrandingFile +{ + param($obj, $prop, $path) + + if(-not $obj.$prop) { return } + + $arr=$obj.$prop.Split('.') + if($arr.Length -ne 1) + { + return + } + $fileType = "jpg" # Assume...not OK. $arr[0] contains information about what kind of file it is + + $fileName = "$path\$((Remove-InvalidFileNameChars "$($obj.localeDisplayName).$prop.$fileType"))" + try + { + if(Test-Path $fileName) + { + Remove-Item -Path $fileName -Force + } + [IO.File]::WriteAllBytes($fileName, [System.Convert]::FromBase64String($arr[1])) + } + catch {} +} + +function Import-AZBranding +{ + param($obj) + + if($global:runningBulkImport -eq $true) + { + # Update Default and create the rest... + $createNew = $obj.locale -ne 0 + } + else + { + $curObj = $global:lstFiles.ItemsSource | Where { $_.Object.locale -eq $obj.locale } + + if($curObj -and $obj.locale -ne 0) + { + return # Do not update existing object except default + } + elseif(-not $curObj) + { + $createNew = $true + } + else + { + $createNew = $false + } + } + + $json = "{" + + if($createNew) { $json += "`"locale`":`"$($obj.locale)`"," } + + if($obj.signInUserIdLabel) { $json += "`"userIdLabel`": `"$($obj.signInUserIdLabel)`"," } + if($obj.signInPageText) { $json += "`"boilerPlateText`": `"$($obj.signInPageText)`"," } + if($obj.signInBackColor) { $json += "`"backgroundColor`": `"$($obj.signInBackColor)`"," } + if($obj.tileLogoUrl) { $json += "`"tileLogoUrl`": `"$($obj.tileLogoUrl)`"," } + if($obj.bannerLogoUrl) { $json += "`"bannerLogoUrl`": `"$($obj.bannerLogoUrl)`"," } + if($obj.illustrationUrl) { $json += "`"illustrationUrl`": `"$($obj.illustrationUrl)`"," } + if($obj.squareLogoDarkUrl) { $json += "`"squareLogoDarkUrl`": `"$($obj.squareLogoDarkUrl)`"," } + + if($obj.hideKeepMeSignedIn -and $obj.locale -eq 0) { $json += "`"keepMeSignedInDisabled`": $($obj.hideKeepMeSignedIn.ToString().ToLower())," } + + if($createNew) + { + if($curObj.bannerLogoUrl -ne $curObj.bannerLogoUrl) + { + $json += "`"isTileLogoUpdated`":true," + } + + if($curObj.illustrationUrl -ne $curObj.illustrationUrl) + { + $json += "`"isIllustrationImageUpdated`":true," + } + + if($curObj.squareLogoDarkUrl -ne $curObj.squareLogoDarkUrl) + { + $json += "`"isSquareDarkLogoUpdated`":true," + } + + if($curObj.bannerLogoUrl -ne $curObj.bannerLogoUrl) + { + $json += "`"isBannerLogoUpdated`":true," + } + } + + $json = $json.TrimEnd(',') + $json += "}" + + Write-Status "Import $($obj.localeDisplayName)" + + if($createNew) + { + Invoke-AzureNativeRequest "LoginTenantBrandings" -Method POST -Body $json | Out-Null + } + else + { + Invoke-AzureNativeRequest "LoginTenantBrandings/$($obj.locale)" -Method PATCH -Body $json | Out-Null + } +} + +function Import-AllAZBrandingObjects +{ + param($path = "$env:Temp") + + Import-AZBrandingObjects (Get-JsonFileObjects $path) +} + +function Import-AZBrandingObjects +{ + param( + $Objects, + + [switch] + $Selected + ) + + Write-Status "Import Azure brandings" + + foreach($obj in $objects) + { + if($Selected -and $obj.Selected -ne $true) { continue } + + Write-Log "Import Azure branding" + + $response = Import-AZBranding $obj.Object + + if($response) + { + $global:importedObjects++ + } + } + $dgObjects.ItemsSource = @(Get-AZBrandingObjects) + Write-Status "" +} \ No newline at end of file diff --git a/Extensions/AzureNative.psm1 b/Extensions/AzureNative.psm1 new file mode 100644 index 0000000..553fd72 --- /dev/null +++ b/Extensions/AzureNative.psm1 @@ -0,0 +1,169 @@ +# +# Azure functions are based on: ??? +# +function Invoke-AzureNativeRequest { + <# + .SYNOPSIS + Runs a command against the Azure Portal API + #> + + [CmdletBinding(SupportsShouldProcess)] + param ( + #The target of your request. This is appended to the Portal API URI. Example: Permissions + [Parameter(Mandatory)]$Target, + + #The command you wish to execute. Example: GetUserSystemRoleTemplateIds + [Parameter()]$Action, + + #The body of your request. This is usually in JSON format + $Body, + + #Specify the HTTP Method you wish to use. Defaults to GET + [ValidateSet("GET","POST","OPTIONS","DELETE", "PATCH", "PUT")] + $Method = "GET", + + #The base URI for the Portal API. Typically you don't need to change this + [Uri]$baseURI = 'https://main.iam.ad.ext.azure.com/api/', + + [URI]$requestOrigin = 'https://iam.hosting.portal.azure.net', + + #The request ID for the session. You can generate one with [guid]::NewGuid().guid. + #Typically you only specify this if you're trying to retry an operation and don't want to duplicate the request, such as for a POST operation + $requestID = [guid]::NewGuid().guid + ) + + #Combine the BaseURI and Target + [String]$ApiAction = $Target.TrimStart('/') + + if ($Action) + { + $ApiAction = $ApiAction + '/' + $Action + } + + if($global:tokresponse -and [DateTimeOffset]::Now.ToUnixTimeSeconds() -gt $global:tokresponse.expires_on) + { + $global:tokresponse = $null + } + + $Context = Get-AzureRmContext + + if(-not $context -or -not $global:tokresponse) + { + if($Context) + { + if($global:Me -and $global:Organization) + { + $refreshToken = ($Context.TokenCache.ReadItems() | Where { $_.DisplayableId -eq $global:Me.userPrincipalName -and $_.TenantId -eq $global:Organization.Id }).RefreshToken + if($refreshToken -and $refreshToken.ExpiresOn -lt (Get-Date)) + { + # Expired...force login + # $refreshToken = $null + } + } + } + + if(-not $refreshToken) + { + $user = Connect-AzureRmAccount + if(-not $user) { return } + $Context = Get-AzureRmContext + if(-not $Context) { return } + } + } + + # + if(-not $global:tokresponse) + { + $refreshToken = $null # Fore read again in case of login + if($global:Me -and $global:Organization) + { + $refreshToken = ($Context.TokenCache.ReadItems() | Where { $_.DisplayableId -eq $global:Me.userPrincipalName -and $_.TenantId -eq $global:Organization.Id }).RefreshToken + } + # Make sure we are using the same user as Intune login + if(-not $refreshToken) + { + [System.Windows.MessageBox]::Show("Failed to find login token for AzureRM", "Invalid AzureRM login!", "OK", "Error") + return $global:tokresponse + } + + $curToken = $Context.TokenCache.ReadItems() | Where { $_.DisplayableId -eq $global:Me.userPrincipalName -and $_.TenantId -eq $global:Organization.Id } + $tenantid = $curToken.TenantId + $refreshToken = $curToken.RefreshToken + $loginUrl = "https://login.windows.net/$tenantid/oauth2/token" + $bodyTmp = "grant_type=refresh_token&refresh_token=$($refreshToken)" #&resource=74658136-14ec-4630-ad9b-26e160ff0fc6" + $response = Invoke-RestMethod $loginUrl -Method POST -Body $bodyTmp -ContentType 'application/x-www-form-urlencoded' + + $global:tokresponse = Invoke-RestMethod $loginUrl -Method POST -Body ($bodyTmp + "&resource=74658136-14ec-4630-ad9b-26e160ff0fc6") + + if(-not $global:tokresponse) { return } + } + + $InvokeRestMethodParams = @{ + Uri = [Uri]::New($baseURI,$ApiAction) + Method = $Method + Header = [ordered]@{ + Authorization = 'Bearer ' + $global:tokresponse.access_token + 'Content-Type' = 'application/json' + 'x-ms-client-request-id' = $requestID + 'Host' = $baseURI.Host + 'Origin' = $requestOrigin + } + Body = $Body + } + + try + { + Invoke-RestMethod @InvokeRestMethodParams + if($? -eq $false) + { + throw $global:error[0] + } + + } + catch + { + Write-LogError "Failed to invoke Invoke-RestMethod for Azure" $_.Exception + } +} + +function Get-AzureNativeObjects +{ + param( + [Array] + $Target, + [Array] + $property, + [Array] + $exclude, + $SortProperty = "") + + $objects = @() + $nativeObjects =Invoke-AzureNativeRequest $Target + + if(($nativeObjects | GM -Name "items")) + { + $objectList = $nativeObjects.Items + } + else + { + $objectList = $nativeObjects + } + + foreach($nativeObject in $objectList) + { + $params = @{} + if($property) { $params.Add("Property", $property) } + if($exclude) { $params.Add("ExcludeProperty", $exclude) } + foreach($objTmp in ($nativeObject | select @params)) + { + $objTmp | Add-Member -NotePropertyName "Object" -NotePropertyValue $nativeObject + $objects += $objTmp + } + } + + if($objects.Count -gt 0 -and $SortProperty -and ($objects[0] | GM -MemberType NoteProperty -Name $SortProperty)) + { + $objects = $objects | sort -Property $SortProperty + } + $objects +} \ No newline at end of file diff --git a/Extensions/Baseline.psm1 b/Extensions/Baseline.psm1 new file mode 100644 index 0000000..c9a0eb1 --- /dev/null +++ b/Extensions/Baseline.psm1 @@ -0,0 +1,294 @@ +######################################################## +# +# Common module functions +# +######################################################## +function Add-ModuleMenuItems +{ + Add-MenuItem (New-Object PSObject -Property @{ + Title = (Get-BaselineTemplatesName) + MenuID = "IntuneGraphAPI" + Script = [ScriptBlock]{Get-BaselineTemplates} + }) + + Add-MenuItem (New-Object PSObject -Property @{ + Title = (Get-BaselineName) + MenuID = "IntuneGraphAPI" + Script = [ScriptBlock]{Get-BaselineProfiles} + }) + +} + +function Get-SupportedImportObjects +{ + $global:importObjects += (New-Object PSObject -Property @{ + Selected = $true + Title = (Get-AppProtectionName) + Script = [ScriptBlock]{ + param($rootFolder) + + Write-Status "Import all baseline policies" + Import-AllBaselineProfileObjects (Join-Path $rootFolder (Get-BaselineFolderName)) + } + }) +} + +function Get-SupportedExportObjects +{ + $global:exportObjects += (New-Object PSObject -Property @{ + Selected = $true + Title = (Get-AppProtectionName) + Folder = (Get-BaselineFolderName) + Script = [ScriptBlock]{ + param($rootFolder) + + Write-Status "Import all baseline policies" + Get-BaselineProfileObjects | ForEach-Object { Export-SingleBaselineProfile $PSItem.Object (Join-Path $rootFolder (Get-BaselineFolderName)) } + } + }) +} + +function Export-AllObjects +{ + param($addObjectSubfolder) + + $subFolder = "" + if($addObjectSubfolder) { $subFolder = Get-BaselineFolderName } + +} +######################################################## +# +# Object specific functions +# +######################################################## +function Get-BaselineTemplatesName +{ + return "Baseline Templates" +} + + +function Get-BaselineName +{ + return "Baseline Profiles" +} + +function Get-BaselineFolderName +{ + return "Baseline" +} + +function Get-BaselineTemplates +{ + Write-Status "Loading baseline templates" -SkipLog + $dgObjects.ItemsSource = @(Get-BaselineTemplateObjects) +} + +function Get-BaselineTemplateObjects +{ + Get-GraphObjects -Url "/deviceManagement/templates" +} + +function Get-BaselineProfiles +{ + Write-Status "Loading banding profiles" -SkipLog + $dgObjects.ItemsSource = @(Get-BaselineProfileObjects) + + #Scriptblocks that will perform the export tasks. empty by default + $script:exportParams = @{} + $script:exportParams.Add("ExportAllScript", [ScriptBlock]{ + Export-AllBaselineProfiles $global:txtExportPath.Text + Set-ObjectGrid + Write-Status "" + }) + + $script:exportParams.Add("ExportSelectedScript", [ScriptBlock]{ + Export-SelectedBaselineProfile $global:txtExportPath.Text + Set-ObjectGrid + Write-Status "" + }) + + #Scriptblock that will perform the import all files + $script:importAll = [ScriptBlock]{ + Import-AllBaselineProfileObjects $global:txtImportPath.Text + Set-ObjectGrid + } + + #Scriptblock that will perform the import of selected files + $script:importSelected = [ScriptBlock]{ + Import-BaselineProfileObjects $global:lstFiles.ItemsSource -Selected + Set-ObjectGrid + } + + #Scriptblock that will read json files + $script:getImportFiles = [ScriptBlock]{ + Show-FileListBox + $global:lstFiles.ItemsSource = @(Get-JsonFileObjects $global:txtImportPath.Text -Exclude @("*_Settings.json","*_assignments.json")) + } + + Add-DefaultObjectButtons -export ([scriptblock]{Show-DefaultExportGrid @script:exportParams}) -import ([scriptblock]{Show-DefaultImportGrid -ImportAll $script:importAll -ImportSelected $script:importSelected -GetFiles $script:getImportFiles}) -copy ([scriptblock]{Copy-BaselineProfile}) +} + +function Get-BaselineProfileObjects +{ + Get-GraphObjects -Url "/deviceManagement/intents" +} + +function Export-AllBaselineProfiles +{ + param($path = "$env:Temp") + + if(-not (Test-Path $path)) { mkdir -Path $path -Force -ErrorAction SilentlyContinue | Out-Null } + + if(Test-Path $path) + { + foreach($objTmp in ($global:dgObjects.ItemsSource)) + { + Export-SingleBaselineProfile $objTmp.Object $path + } + } +} + +function Export-SelectedBaselineProfile +{ + param($path = "$env:Temp") + + Export-SingleBaselineProfile $global:dgObjects.SelectedItem.Object $path +} + +function Export-SingleBaselineProfile +{ + param($psObj, $path = "$env:Temp") + + if(-not $psObj) { return } + + if($global:runningBulkExport -ne $true) + { + if($global:chkAddCompanyName.IsChecked) { $path = Join-Path $path $global:organization.displayName } + if($global:chkAddObjectType.IsChecked) { $path = Join-Path $path (Get-BaselineFolderName) } + } + + if(-not (Test-Path $path)) { mkdir -Path $path -Force -ErrorAction SilentlyContinue | Out-Null } + + if(Test-Path $path) + { + Write-Status "Export $($psObj.displayName)" + $obj = $psObj + if($obj) + { + $fileName = "$path\$((Remove-InvalidFileNameChars $obj.displayName)).json" + ConvertTo-Json $obj -Depth 5 | Out-File $fileName -Force + $settings = Invoke-GraphRequest -Url "/deviceManagement/intents/$($obj.id)/settings" + ConvertTo-Json $settings.value -Depth 5 | Out-File "$path\$($obj.displayName)_Settings.json" -Force + } + $assignments = Invoke-GraphRequest -Url "/deviceManagement/intents/$($obj.id)/assignments" + if(($assignments.Value | measure).Count -gt 0) + { + ConvertTo-Json $assignments.value -Depth 5| Out-File "$path\$($obj.displayName)_assignments.json" -Force + } + $global:exportedObjects++ + } +} + +function Copy-BaselineProfile +{ + if(-not $dgObjects.SelectedItem) + { + [System.Windows.MessageBox]::Show("No object selected`n`nSelect baseline profile you want to copy", "Error", "OK", "Error") + return + } + + $ret = Show-InputDialog "Copy baseline profiles" "Select name for the new object" "$($dgObjects.SelectedItem.displayName) - Copy" + + if($ret) + { + # Export profile + Write-Status "Export $($dgObjects.SelectedItem.displayName)" + # Convert to Json and back to clone the object + $obj = ConvertTo-Json $dgObjects.SelectedItem.Object -Depth 5 | ConvertFrom-Json + $settings = Invoke-GraphRequest -Url "/deviceManagement/intents/$($obj.id)/settings" + $intentSettings = ConvertTo-Json $settings.value -Depth 5 + + if($obj) + { + # Import new profile + $obj.displayName = $ret + Import-BaselineProfile $obj $intentSettings | Out-null + + $dgObjects.ItemsSource = @(Get-BaselineProfileObjects) + } + Write-Status "" + } + $dgObjects.Focus() +} + +function Import-BaselineProfile +{ + param($obj, $intentSettings, $templateId) + +$json = @" + { + "displayName": "$($obj.displayName)", + "description": "$($obj.description)", + "settingsDelta": + $($intentSettings) + + } +"@ + + if($templateId) + { + $tempId = $templateId + } + else + { + $tempId = $obj.templateId + } + + Write-Status "Import $($obj.displayName)" + + return Invoke-GraphRequest -Url "/deviceManagement/templates/$($tempId)/createInstance" -Content $json -HttpMethod POST +} + +function Import-AllBaselineProfileObjects +{ + param( + $path = "$env:Temp" + ) + + Import-BaselineProfileObjects (Get-JsonFileObjects $path) +} + +function Import-BaselineProfileObjects +{ + param( + $Objects, + + [switch] + $Selected + ) + + Write-Status "Import terms and conditions" + + foreach($obj in $objects) + { + if($Selected -and $obj.Selected -ne $true) { continue } + + Write-Log "Import security baseline: $($obj.Object.displayName)" + + $assignments = Get-GraphAssignmentsObject $obj.Object ($obj.FileInfo.DirectoryName + "\" + $obj.FileInfo.BaseName + "_assignments.json") + + $settingsFile = $obj.FileInfo.DirectoryName + "\" + $obj.FileInfo.BaseName + "_settings.json" + if(-not (Test-Path $settingsFile)) { continue } + + $intentSettings = Get-Content $settingsFile -Raw + + $response = Import-BaselineProfile $obj.Object $intentSettings + if($response) + { + $global:importedObjects++ + Import-GraphAssignments $assignments "assignments" "/deviceManagement/intents/$($response.Id)/assign" + } + } + $dgObjects.ItemsSource = @(Get-BaselineProfileObjects) + Write-Status "" +} diff --git a/Extensions/Branding.psm1 b/Extensions/Branding.psm1 new file mode 100644 index 0000000..ec20ce4 --- /dev/null +++ b/Extensions/Branding.psm1 @@ -0,0 +1,236 @@ +######################################################## +# +# Common module functions +# +######################################################## +function Add-ModuleMenuItems +{ + Add-MenuItem (New-Object PSObject -Property @{ + Title = (Get-IntuneBrandingName) + MenuID = "IntuneGraphAPI" + Script = [ScriptBlock]{Get-IntuneBrandings} + }) +} + +function Get-SupportedImportObjects +{ + $global:importObjects += (New-Object PSObject -Property @{ + Selected = $true + Title = (Get-IntuneBrandingName) + Script = [ScriptBlock]{ + param($rootFolder) + + Write-Status "Import all Intune branding objects" + Import-AllIntuneBrandingObjects (Join-Path $rootFolder (Get-IntuneBrandingFolderName)) + } + }) +} + +function Get-SupportedExportObjects +{ + $global:exportObjects += (New-Object PSObject -Property @{ + Selected = $true + Title = (Get-IntuneBrandingName) + Script = [ScriptBlock]{ + param($rootFolder) + + Write-Status "Export all Intune branding objects" + Get-IntuneBrandingObjects | ForEach-Object { Export-SingleIntuneBranding $PSItem.Object (Join-Path $rootFolder (Get-IntuneBrandingFolderName)) } + } + }) +} + +function Export-AllObjects +{ + param($addObjectSubfolder) + + $subFolder = "" + if($addObjectSubfolder) { $subFolder = Get-IntuneBrandingFolderName } +} + +######################################################## +# +# Object specific functions +# +######################################################## +function Get-IntuneBrandingName +{ + return "Intune Branding" +} + +function Get-IntuneBrandingFolderName +{ + return "IntuneBranding" +} + +function Get-IntuneBrandings +{ + Write-Status "Loading banding profiles" + $dgObjects.ItemsSource = @(Get-IntuneBrandingObjects) + + #Scriptblocks that will perform the export tasks. empty by default + $script:exportParams = @{} + $script:exportParams.Add("ExportAllScript", [ScriptBlock]{ + Export-AllIntuneBrandings $global:txtExportPath.Text + Set-ObjectGrid + Write-Status "" + }) + + # Same as ExportAllScript since only one object is supported + $script:exportParams.Add("ExportSelectedScript", [ScriptBlock]{ + Export-AllIntuneBrandings $global:txtExportPath.Text + Set-ObjectGrid + Write-Status "" + }) + + #Scriptblock that will perform the import all files + $script:importAll = [ScriptBlock]{ + Import-AllIntuneBrandingObjects $global:txtImportPath.Text + Set-ObjectGrid + } + + #Scriptblock that will perform the import of selected files + $script:importSelected = [ScriptBlock]{ + Import-IntuneBrandingObjects $global:lstFiles.ItemsSource -Selected + Set-ObjectGrid + } + + #Scriptblock that will read json files + $script:getImportFiles = [ScriptBlock]{ + Show-FileListBox + $global:lstFiles.ItemsSource = @(Get-JsonFileObjects $global:txtImportPath.Text -Exclude "*_Settings.json") + } + + Add-DefaultObjectButtons -export ([scriptblock]{Show-DefaultExportGrid @script:exportParams}) -import ([scriptblock]{Show-DefaultImportGrid -ImportAll $script:importAll -ImportSelected $script:importSelected -GetFiles $script:getImportFiles}) +} + +function Get-IntuneBrandingObjects +{ + Get-GraphObjects -Url "/deviceManagement/intuneBrand" -property @("displayName") +} + +function Export-AllIntuneBrandings +{ + param($path = "$env:Temp") + + if(-not (Test-Path $path)) { mkdir -Path $path -Force -ErrorAction SilentlyContinue | Out-Null } + + if(Test-Path $path) + { + foreach($objTmp in ($global:dgObjects.ItemsSource)) + { + Export-SingleIntuneBranding $objTmp.Object $path + } + } +} + +function Export-SingleIntuneBranding +{ + param($psObj, $path = "$env:Temp") + + if(-not $psObj) { return } + + if($global:runningBulkExport -ne $true) + { + if($global:chkAddCompanyName.IsChecked) { $path = Join-Path $path $global:organization.displayName } + if($global:chkAddObjectType.IsChecked) { $path = Join-Path $path (Get-IntuneBrandingFolderName) } + } + + if(-not (Test-Path $path)) { mkdir -Path $path -Force -ErrorAction SilentlyContinue | Out-Null } + + if(Test-Path $path) + { + Write-Status "Export $($psObj.displayName)" + + $obj = $psObj + if($obj) + { + $fileName = "$path\$((Remove-InvalidFileNameChars $obj.displayName)).json" + ConvertTo-Json $obj -Depth 5 | Out-File $fileName -Force + Save-IntuneBrandingFile $obj "lightBackgroundLogo" $path + Save-IntuneBrandingFile $obj "darkBackgroundLogo" $path + Save-IntuneBrandingFile $obj "landingPageCustomizedImage" $path + } + $global:exportedObjects++ + } +} + +function Save-IntuneBrandingFile +{ + param($obj, $prop, $path) + + if(-not $obj.$prop.type) { return } + + $arr=$obj.$prop.type.Split('/') + if($arr.Length -gt 1) + { + $fileType = $arr[1] + } + else + { + $fileType = ".jpg" # assume... + } + + $fileName = "$path\$((Remove-InvalidFileNameChars "$($obj.displayName).$prop.$fileType"))" + try + { + if(Test-Path $fileName) + { + Remove-Item -Path $fileName -Force + } + [IO.File]::WriteAllBytes($fileName, [System.Convert]::FromBase64String($obj.$prop.value)) + } + catch {} +} + + +function Import-IntuneBranding +{ + param($obj) + + Remove-ObjectProperty $obj "@odata.context" + + $newObject = @" +{ + "intuneBrand":$((ConvertTo-Json $obj -Depth 5)) +} + +"@ + Write-Status "Import $($obj.displayName)" + + # Note: Branding is imported to deviceManagement with JSON parent object intuneBrand + Invoke-GraphRequest -Url "$URL/deviceManagement" -Content $newObject -HttpMethod PATCH +} + +function Import-AllIntuneBrandingObjects +{ + param($path = "$env:Temp") + + Import-IntuneBrandingObjects (Get-JsonFileObjects $path) +} + +function Import-IntuneBrandingObjects +{ + param( + $Objects, + + [switch] + $Selected + ) + + Write-Status "Import Intune branding" + + foreach($obj in $objects) + { + if($Selected -and $obj.Selected -ne $true) { continue } + + Write-Log "Import Intune branding" + + $response = Import-IntuneBranding $obj.Object + + # Note: No assignments for branding. This is default branding for everyone + + } + $dgObjects.ItemsSource = @(Get-IntuneBrandingObjects) + Write-Status "" +} \ No newline at end of file diff --git a/Extensions/CompliancePolicies.psm1 b/Extensions/CompliancePolicies.psm1 new file mode 100644 index 0000000..8598de9 --- /dev/null +++ b/Extensions/CompliancePolicies.psm1 @@ -0,0 +1,244 @@ +######################################################## +# +# Common module functions +# +######################################################## +function Add-ModuleMenuItems +{ + Add-MenuItem (New-Object PSObject -Property @{ + Title = (Get-CompliancePolicyName) + MenuID = "IntuneGraphAPI" + Script = [ScriptBlock]{Get-CompliancePolicies} + }) +} + +function Get-SupportedImportObjects +{ + $global:importObjects += (New-Object PSObject -Property @{ + Selected = $true + Title = (Get-CompliancePolicyName) + Script = [ScriptBlock]{ + param($rootFolder) + + Write-Status "Import all Intune compliance policies" + Import-AllCompliancePolicyObjects (Join-Path $rootFolder (Get-CompliancePolicyFolderName)) + } + }) +} + +function Get-SupportedExportObjects +{ + $global:exportObjects += (New-Object PSObject -Property @{ + Selected = $true + Title = (Get-CompliancePolicyName) + Script = [ScriptBlock]{ + param($rootFolder) + + Write-Status "Export all compliance policies" + Get-CompliancePolicyObjects | ForEach-Object { Export-SingleCompliancePolicy $PSItem.Object (Join-Path $rootFolder (Get-CompliancePolicyFolderName)) } + } + }) +} + +function Export-AllObjects +{ + param($addObjectSubfolder) + + $subFolder = "" + if($addObjectSubfolder) { $subFolder = Get-CompliancePolicyFolderName } +} + +######################################################## +# +# Object specific functions +# +######################################################## +function Get-CompliancePolicyName +{ + return "Compliance Policies" +} + +function Get-CompliancePolicyFolderName +{ + return "CompliancePolicies" +} + +function Get-CompliancePolicies +{ + Write-Status "Loading compliance policies" + $dgObjects.ItemsSource = @(Get-CompliancePolicyObjects) + + #Scriptblocks that will perform the export tasks. empty by default + $script:exportParams = @{} + $script:exportParams.Add("ExportAllScript", [ScriptBlock]{ + Export-AllCompliancePolicies $global:txtExportPath.Text + Set-ObjectGrid + Write-Status "" + }) + + $script:exportParams.Add("ExportSelectedScript", [ScriptBlock]{ + Export-SelectedCompliancePolicy $global:txtExportPath.Text + Set-ObjectGrid + Write-Status "" + }) + + #Scriptblock that will perform the import all files + $script:importAll = [ScriptBlock]{ + Import-AllCompliancePolicyObjects $global:txtImportPath.Text + Set-ObjectGrid + } + + #Scriptblock that will perform the import of selected files + $script:importSelected = [ScriptBlock]{ + Import-CompliancePolicyObjects $global:lstFiles.ItemsSource -Selected + Set-ObjectGrid + } + + #Scriptblock that will read json files + $script:getImportFiles = [ScriptBlock]{ + Show-FileListBox + $global:lstFiles.ItemsSource = @(Get-JsonFileObjects $global:txtImportPath.Text -Exclude "*_Settings.json") + } + + Add-DefaultObjectButtons -export ([scriptblock]{Show-DefaultExportGrid @script:exportParams}) -import ([scriptblock]{Show-DefaultImportGrid -ImportAll $script:importAll -ImportSelected $script:importSelected -GetFiles $script:getImportFiles}) -copy ([scriptblock]{Copy-CompliancePolicy}) +} + +function Get-CompliancePolicyObjects +{ + Get-GraphObjects -Url "/deviceManagement/deviceCompliancePolicies" +} + +function Export-AllCompliancePolicies +{ + param($path = "$env:Temp") + + if(-not (Test-Path $path)) { mkdir -Path $path -Force -ErrorAction SilentlyContinue | Out-Null } + + if(Test-Path $path) + { + foreach($objTmp in ($global:dgObjects.ItemsSource)) + { + Export-SingleCompliancePolicy $objTmp.Object $path + } + } +} + +function Export-SelectedCompliancePolicy +{ + param($path = "$env:Temp") + + Export-SingleCompliancePolicy $global:dgObjects.SelectedItem.Object $path +} + +function Export-SingleCompliancePolicy +{ + param($psObj, $path = "$env:Temp") + + if(-not $psObj) { return } + + if($global:runningBulkExport -ne $true) + { + if($global:chkAddCompanyName.IsChecked) { $path = Join-Path $path $global:organization.displayName } + if($global:chkAddObjectType.IsChecked) { $path = Join-Path $path (Get-CompliancePolicyFolderName) } + } + + if(-not (Test-Path $path)) { mkdir -Path $path -Force -ErrorAction SilentlyContinue | Out-Null } + + if(Test-Path $path) + { + Write-Status "Export $($psObj.displayName)" + $obj = Invoke-GraphRequest -Url "/deviceManagement/deviceCompliancePolicies/$($psObj.id)?`$expand=assignments" + if($obj) + { + $fileName = "$path\$((Remove-InvalidFileNameChars $obj.displayName)).json" + ConvertTo-Json $obj -Depth 5 | Out-File $fileName -Force + + Add-MigrationInfo $obj.assignments + } + $global:exportedObjects++ + } +} + +function Copy-CompliancePolicy +{ + if(-not $dgObjects.SelectedItem) + { + [System.Windows.MessageBox]::Show("No object selected`n`nSelect compliance policy item you want to copy", "Error", "OK", "Error") + return + } + + $ret = Show-InputDialog "Copy compliance policy" "Select name for the new object" "$($dgObjects.SelectedItem.displayName) - Copy" + + if($ret) + { + # Export profile + Write-Status "Export $($dgObjects.SelectedItem.displayName)" + # Convert to Json and back to clone the object + $obj = ConvertTo-Json $dgObjects.SelectedItem.Object -Depth 5 | ConvertFrom-Json + if($obj) + { + # Import new profile + $obj.displayName = $ret + Import-CompliancePolicy $obj | Out-null + + $dgObjects.ItemsSource = @(Get-CompliancePolicyObjects) + } + Write-Status "" + } + $dgObjects.Focus() +} + +function Import-CompliancePolicy +{ + param($obj) + + $json = ConvertTo-Json $obj -Depth 5 + $json = $json.Trim().TrimEnd('}').Trim() + $json += @" +, + "scheduledActionsForRule":[{"ruleName":"PasswordRequired","scheduledActionConfigurations":[{"actionType":"block","gracePeriodHours":0,"notificationTemplateId":"","notificationMessageCCList":[]}]}] +} + +"@ + + Write-Status "Import $($obj.displayName)" + + Invoke-GraphRequest -Url "/deviceManagement/deviceCompliancePolicies" -Content $json -HttpMethod POST +} + +function Import-AllCompliancePolicyObjects +{ + param($path = "$env:Temp") + + Import-CompliancePolicyObjects (Get-JsonFileObjects $path) +} + +function Import-CompliancePolicyObjects +{ + param( + $Objects, + + [switch] + $Selected + ) + + Write-Status "Import compliance policies" + + foreach($obj in $objects) + { + if($Selected -and $obj.Selected -ne $true) { continue } + + Write-Log "Import Compliance Policy: $($obj.Object.displayName)" + + $assignments = Get-GraphAssignmentsObject $obj.Object ($obj.FileInfo.DirectoryName + "\" + $obj.FileInfo.BaseName + "_assignments.json") + + $response = Import-CompliancePolicy $obj.Object + if($response) + { + $global:importedObjects++ + Import-GraphAssignments $assignments "assignments" "/deviceManagement/deviceCompliancePolicies/$($response.Id)/assign" + } + } + $dgObjects.ItemsSource = @(Get-CompliancePolicyObjects) + Write-Status "" +} \ No newline at end of file diff --git a/Extensions/ConditionalAccess.psm1 b/Extensions/ConditionalAccess.psm1 new file mode 100644 index 0000000..c8aabd7 --- /dev/null +++ b/Extensions/ConditionalAccess.psm1 @@ -0,0 +1,273 @@ +######################################################## +# +# Common module functions +# +######################################################## +function Add-ModuleMenuItems +{ + Add-MenuItem (New-Object PSObject -Property @{ + Title = (Get-ConditionalAccessName) + MenuID = "IntuneGraphAPI" + Script = [ScriptBlock]{Get-ConditionalAccess} + }) +} + +function Get-SupportedImportObjects +{ + $global:importObjects += (New-Object PSObject -Property @{ + Selected = $true + Title = (Get-ConditionalAccessName) + Script = [ScriptBlock]{ + param($rootFolder) + + Write-Status "Import all conditional access policies" + Import-AllConditionalAccessObjects (Join-Path $rootFolder (Get-ConditionalAccessFolderName)) + } + }) +} + +function Get-SupportedExportObjects +{ + $global:exportObjects += (New-Object PSObject -Property @{ + Selected = $true + Title = (Get-ConditionalAccessName) + Script = [ScriptBlock]{ + param($rootFolder) + + Write-Status "Export all conditional access policies" + Get-ConditionalAccessObjects | ForEach-Object { Export-SingleConditionalAccess $PSItem.Object (Join-Path $rootFolder (Get-ConditionalAccessFolderName)) } + } + }) +} + +function Export-AllObjects +{ + param($addObjectSubfolder) + + $subFolder = "" + if($addObjectSubfolder) { $subFolder = Get-ConditionalAccessFolderName } +} + +######################################################## +# +# Object specific functions +# +######################################################## +function Get-ConditionalAccessName +{ + return "Conditional Access" +} + +function Get-ConditionalAccessFolderName +{ + return "ConditionalAccess" +} + +function Get-ConditionalAccess +{ + Write-Status "Loading conditional access objects" + $dgObjects.ItemsSource = @(Get-ConditionalAccessObjects) + + #Scriptblocks that will perform the export tasks. empty by default + $script:exportParams = @{} + $script:exportParams.Add("ExportAllScript", [ScriptBlock]{ + Export-AllConditionalAccess $global:txtExportPath.Text + Set-ObjectGrid + Write-Status "" + }) + + $script:exportParams.Add("ExportSelectedScript", [ScriptBlock]{ + Export-SelectedConditionalAccess $global:txtExportPath.Text + Set-ObjectGrid + Write-Status "" + }) + #Scriptblock that will perform the import all files + $script:importAll = [ScriptBlock]{ + Import-AllConditionalAccessObjects $global:txtImportPath.Text + Set-ObjectGrid + } + + #Scriptblock that will perform the import of selected files + $script:importSelected = [ScriptBlock]{ + Import-ConditionalAccessObjects $global:lstFiles.ItemsSource -Selected + Set-ObjectGrid + } + + #Scriptblock that will read json files + $script:getImportFiles = [ScriptBlock]{ + Show-FileListBox + $global:lstFiles.ItemsSource = @(Get-JsonFileObjects $global:txtImportPath.Text -Exclude "*_Settings.json") + } + + Add-DefaultObjectButtons -export ([scriptblock]{Show-DefaultExportGrid @script:exportParams}) -import ([scriptblock]{Show-DefaultImportGrid -ImportAll $script:importAll -ImportSelected $script:importSelected -GetFiles $script:getImportFiles}) +} + +function Get-ConditionalAccessObjects +{ + #https://main.iam.ad.ext.azure.com/api/Policies/Policies?top=10&nextLink=null&appId=&includeBaseline=true + Get-AzureNativeObjects "Policies/Policies?top=10&nextLink=null&appId=&includeBaseline=true" -property @('policyName') +} + +function Export-AllConditionalAccess +{ + param($path = "$env:Temp") + + if(-not (Test-Path $path)) { mkdir -Path $path -Force -ErrorAction SilentlyContinue | Out-Null } + + if(Test-Path $path) + { + foreach($objTmp in ($global:dgObjects.ItemsSource)) + { + Export-SingleConditionalAccess $objTmp.Object $path + } + } +} + +function Export-SelectedConditionalAccess +{ + param($path = "$env:Temp") + + Export-SingleConditionalAccess $global:dgObjects.SelectedItem.Object $path +} + +function Export-SingleConditionalAccess +{ + param($psObj, $path = "$env:Temp") + + if(-not $psObj) { return } + + if($global:runningBulkExport -ne $true) + { + if($global:chkAddCompanyName.IsChecked) { $path = Join-Path $path $global:organization.displayName } + if($global:chkAddObjectType.IsChecked) { $path = Join-Path $path (Get-ConditionalAccessFolderName) } + } + + if(-not (Test-Path $path)) { mkdir -Path $path -Force -ErrorAction SilentlyContinue | Out-Null } + + if(Test-Path $path) + { + Write-Status "Export $($psObj.policyName)" + + if($psObj.baselineType -eq 0) + { + $obj = Invoke-AzureNativeRequest "Policies/$($psObj.policyId)" + } + else + { + $obj = Invoke-AzureNativeRequest "BaselinePolicies/$($psObj.policyId)" + } + + if($obj) + { + $fileName = "$path\$((Remove-InvalidFileNameChars $psObj.policyName)).json" + ConvertTo-Json $obj -Depth 5 | Out-File $fileName -Force + } + + if($jsonObj.usersV2.included.groupIds) + { + $jsonObj.usersV2.included.groupIds | ForEach-Object { Add-GroupMigrationObject $PSItem } + } + + if($jsonObj.usersV2.excluded.groupIds) + { + $jsonObj.usersV2.excluded.groupIds | ForEach-Object { Add-GroupMigrationObject $PSItem } + } + + if($jsonObj.usersV2.included.userIds -or $jsonObj.usersV2.excluded.userIds) + { + Write-Log "Users are specified in $($psObj.policyName). User are not supported in this version. This conditional access policy might not be imported" 2 + } + + if($jsonObj.usersV2.included.roleIds -or $jsonObj.usersV2.excluded.roleIds) + { + Write-Log "Roles are specified in $($psObj.policyName). Roles are not supported in this version. This conditional access policy might not be imported" 2 + } + + if($jsonObj.conditions.namedNetworks.includedNetworkIds -or $jsonObj.conditions.namedNetworks.excludedNetworkIds) + { + Write-Log "Networks are specified in $($psObj.policyName). Named networks are not supported in this version. This conditional access policy might not be imported" 2 + } + + # There might be a lot more to check here... + + $global:exportedObjects++ + } +} + +function Import-ConditionalAccess +{ + param($obj) + + $json = Update-JsonForEnvironment $json + + if($obj.baselineType -eq 0) + { + $obj.policyId = "" + $obj.isAllProtocolsEnabled = $true + $json = ConvertTo-Json $obj -Depth 10 + $json = Update-JsonForEnvironment $json + + if((Invoke-AzureNativeRequest "Policies/Validate" -Method POST -Body $json) -eq 11) + { + Invoke-AzureNativeRequest "Policies" -Method POST -Body $json | Out-Null + } + else + { + Write-Log "Policy validation of json data failed" 3 + } + } + else + { + Write-Log "Conditional Access Baseline Policies does not support import" + #Invoke-AzureNativeRequest "BaselinePolicies/$($obj.id)" -Method PUT -Body (ConvertTo-Json $obj -Depth 5) | Out-Null + } +} + +function Import-AllConditionalAccessObjects +{ + param($path = "$env:Temp") + + Import-ConditionalAccessObjects (Get-JsonFileObjects $path) +} + +function Import-ConditionalAccessObjects +{ + param( + $Objects, + + [switch] + $Selected + ) + + Write-Status "Import conditional access policies" + + foreach($obj in $objects) + { + if($Selected -and $obj.Selected -ne $true) { continue } + + Write-Log "Import Conditional Access: $($obj.Object.policyName)" + + $response = Import-ConditionalAccess $obj.Object + + if($response) + { + $global:importedObjects++ + } + # No additionl assignments on conditional access policies + } + $dgObjects.ItemsSource = @(Get-ConditionalAccessObjects) + Write-Status "" +} + +<# + # Get all networks + Get-AzureNativeObjects "NamedNetworksV2" + + # Network example + #{"networkName":"Australia","cidrIpRanges":[],"categories":[],"applyToUnknownCountry":false,"countryIsoCodes":["AU"],"isTrustedLocation":false,"namedLocationsType":2} + + Get-AzureNativeObjects "NamedNetworksV2" -Method POST -Body $json | Out-Nul + + # Get all contry codes + NamedNetworksV2/CountryCodes +#> \ No newline at end of file diff --git a/Extensions/ConfigurationItems.psm1 b/Extensions/ConfigurationItems.psm1 new file mode 100644 index 0000000..23543d0 --- /dev/null +++ b/Extensions/ConfigurationItems.psm1 @@ -0,0 +1,246 @@ +######################################################## +# +# Common module functions +# +######################################################## +function Add-ModuleMenuItems +{ + Add-MenuItem (New-Object PSObject -Property @{ + Title = (Get-DeviceConfigurationName) + MenuID = "IntuneGraphAPI" + Script = [ScriptBlock]{Get-DeviceConfigurations} + }) +} + +function Get-SupportedImportObjects +{ + $global:importObjects += (New-Object PSObject -Property @{ + Selected = $true + Title = (Get-DeviceConfigurationName) + Script = [ScriptBlock]{ + param($rootFolder) + + Write-Status "Import all device configuration objects" + Import-AllDeviceConfigurationObjects (Join-Path $rootFolder (Get-DeviceConfigurationFolderName)) + } + }) +} + +function Get-SupportedExportObjects +{ + $global:exportObjects += (New-Object PSObject -Property @{ + Selected = $true + Title = (Get-DeviceConfigurationName) + Script = [ScriptBlock]{ + param($rootFolder) + + Write-Status "Export all device configuration objects" + Get-DeviceConfigurationObjects | ForEach-Object { Export-SingleDeviceConfiguration $PSItem.Object (Join-Path $rootFolder (Get-DeviceConfigurationFolderName)) } + } + }) +} + +function Export-AllObjects +{ + param($addObjectSubfolder) + + $subFolder = "" + if($addObjectSubfolder) { $subFolder = Get-DeviceConfigurationFolderName } +} + +######################################################## +# +# Object specific functions +# +######################################################## +function Get-DeviceConfigurationName +{ + return "Device Configurations" +} + +function Get-DeviceConfigurationFolderName +{ + return "DeviceConfigurations" +} + +function Get-DeviceConfigurations +{ + Write-Status "Loading device configurations" + $dgObjects.ItemsSource = @(Get-DeviceConfigurationObjects) + + #Scriptblocks that will perform the export tasks. empty by default + $script:exportParams = @{} + $script:exportParams.Add("ExportAllScript", [ScriptBlock]{ + Export-AllDeviceConfigurations $global:txtExportPath.Text + Set-ObjectGrid + Write-Status "" + }) + + $script:exportParams.Add("ExportSelectedScript", [ScriptBlock]{ + Export-SelectedDeviceConfiguration $global:txtExportPath.Text + Set-ObjectGrid + Write-Status "" + }) + #Scriptblock that will perform the import all files + $script:importAll = [ScriptBlock]{ + Import-AllDeviceConfigurationObjects $global:txtImportPath.Text + Set-ObjectGrid + } + + #Scriptblock that will perform the import of selected files + $script:importSelected = [ScriptBlock]{ + Import-DeviceConfigurationObjects $global:lstFiles.ItemsSource -Selected + Set-ObjectGrid + } + + #Scriptblock that will read json files + $script:getImportFiles = [ScriptBlock]{ + Show-FileListBox + $global:lstFiles.ItemsSource = @(Get-JsonFileObjects $global:txtImportPath.Text -Exclude "*_Settings.json") + } + + Add-DefaultObjectButtons -export ([scriptblock]{Show-DefaultExportGrid @script:exportParams}) -import ([scriptblock]{Show-DefaultImportGrid -ImportAll $script:importAll -ImportSelected $script:importSelected -GetFiles $script:getImportFiles}) -copy ([scriptblock]{Copy-DeviceConfiguration}) +} + +function Get-DeviceConfigurationObjects +{ + Get-GraphObjects -Url "/deviceManagement/deviceConfigurations"#,"/deviceManagement/groupPolicyConfigurations" +} + +function Export-AllDeviceConfigurations +{ + param($path = "$env:Temp") + + if(-not (Test-Path $path)) { mkdir -Path $path -Force -ErrorAction SilentlyContinue | Out-Null } + + if(Test-Path $path) + { + foreach($objTmp in ($global:dgObjects.ItemsSource)) + { + Export-SingleDeviceConfiguration $objTmp.Object $path + } + } +} + +function Export-SelectedDeviceConfiguration +{ + param($path = "$env:Temp") + + Export-SingleDeviceConfiguration $global:dgObjects.SelectedItem.Object $path +} + +function Export-SingleDeviceConfiguration +{ + param($psObj, $path = "$env:Temp") + + if(-not $psObj) { return } + + if($global:runningBulkExport -ne $true) + { + if($global:chkAddCompanyName.IsChecked) { $path = Join-Path $path $global:organization.displayName } + if($global:chkAddObjectType.IsChecked) { $path = Join-Path $path (Get-DeviceConfigurationFolderName) } + } + + if(-not (Test-Path $path)) { mkdir -Path $path -Force -ErrorAction SilentlyContinue | Out-Null } + + if(Test-Path $path) + { + Write-Status "Export $($psObj.displayName)" + $obj = Invoke-GraphRequest -Url "/deviceManagement/deviceConfigurations/$($psObj.id)?`$expand=assignments" + if($obj) + { + $fileName = "$path\$((Remove-InvalidFileNameChars $obj.displayName)).json" + ConvertTo-Json $obj -Depth 5 | Out-File $fileName -Force + if($script:chkExportScript.IsChecked) + { + $fileName = "$path\$($obj.FileName)" + [System.Text.Encoding]::ASCII.GetString([System.Convert]::FromBase64String($obj.scriptContent)) | Out-File $fileName -Force + } + + Add-MigrationInfo $obj.assignments + + $global:exportedObjects++ + } + } +} + +function Copy-DeviceConfiguration +{ + if(-not $dgObjects.SelectedItem) + { + [System.Windows.MessageBox]::Show("No object selected`n`nSelect device configuration item you want to copy", "Error", "OK", "Error") + return + } + + $ret = Show-InputDialog "Copy device configuration" "Select name for the new object" "$($dgObjects.SelectedItem.displayName) - Copy" + + if($ret) + { + # Export profile + Write-Status "Export $($dgObjects.SelectedItem.displayName)" + # Convert to Json and back to clone the object + $obj = ConvertTo-Json $dgObjects.SelectedItem.Object -Depth 5 | ConvertFrom-Json + if($obj) + { + # Import new profile + $obj.displayName = $ret + Import-DeviceConfiguration $obj | Out-Null + + $dgObjects.ItemsSource = @(Get-DeviceConfigurationObjects) + } + Write-Status "" + } + $dgObjects.Focus() +} + +function Import-DeviceConfiguration +{ + param($obj) + + if(($obj | GM -MemberType NoteProperty -Name "supportsScopeTags")) + { + # Remove read-only property + $obj.PSObject.Properties.Remove('supportsScopeTags') + } + + Write-Status "Import $($obj.displayName)" + + Invoke-GraphRequest -Url "/deviceManagement/deviceConfigurations" -Content (ConvertTo-Json $obj -Depth 5) -HttpMethod POST +} + +function Import-AllDeviceConfigurationObjects +{ + param($path = "$env:Temp") + + Import-DeviceConfigurationObjects (Get-JsonFileObjects $path) +} + +function Import-DeviceConfigurationObjects +{ + param( + $Objects, + + [switch] + $Selected + ) + + Write-Status "Import device configuration profiles" + + foreach($obj in $objects) + { + if($Selected -and $obj.Selected -ne $true) { continue } + + Write-Log "Import device configuration policy: $($obj.Object.displayName)" + + $assignments = Get-GraphAssignmentsObject $obj.Object ($obj.FileInfo.DirectoryName + "\" + $obj.FileInfo.BaseName + "_assignments.json") + + $response = Import-DeviceConfiguration $obj.Object + if($response) + { + $global:importedObjects++ + Import-GraphAssignments $assignments "assignments" "/deviceManagement/deviceConfigurations/$($response.Id)/assign" + } + } + $dgObjects.ItemsSource = @(Get-DeviceConfigurationObjects) + Write-Status "" +} \ No newline at end of file diff --git a/Extensions/EnrollmentStatusPage.psm1 b/Extensions/EnrollmentStatusPage.psm1 new file mode 100644 index 0000000..b8e7a57 --- /dev/null +++ b/Extensions/EnrollmentStatusPage.psm1 @@ -0,0 +1,277 @@ +######################################################## +# +# Common module functions +# +######################################################## +function Add-ModuleMenuItems +{ + Add-MenuItem (New-Object PSObject -Property @{ + Title = (Get-ESPName) + MenuID = "IntuneGraphAPI" + Script = [ScriptBlock]{Get-ESPs} + }) +} + +function Get-SupportedImportObjects +{ + $global:importObjects += (New-Object PSObject -Property @{ + Selected = $true + Title = (Get-ESPName) + Script = [ScriptBlock]{ + param($rootFolder) + + Write-Status "Import all enrollment status page settings" + Import-AllESPObjects (Join-Path $rootFolder (Get-ESPFolderName)) + } + }) +} + +function Get-SupportedExportObjects +{ + $global:exportObjects += (New-Object PSObject -Property @{ + Selected = $true + Title = (Get-ESPName) + Script = [ScriptBlock]{ + param($rootFolder) + + Write-Status "Export all enrollment status page settings" + Get-ESPObjects | ForEach-Object { Export-SingleESP $PSItem.Object (Join-Path $rootFolder (Get-ESPFolderName)) } + } + }) +} + +function Export-AllObjects +{ + param($addObjectSubfolder) + + $subFolder = "" + if($addObjectSubfolder) { $subFolder = Get-ESPFolderName } +} + +######################################################## +# +# Object specific functions +# +######################################################## +function Get-ESPName +{ + return "Enrollment Status Page" +} + + +function Get-ESPFolderName +{ + return "EnrollmentStatusPage" +} + +function Get-ESPs +{ + Write-Status "Loading enrollment status page objects" + $dgObjects.ItemsSource = @(Get-ESPObjects) + + #Scriptblocks that will perform the export tasks. empty by default + $script:exportParams = @{} + $script:exportParams.Add("ExportAllScript", [ScriptBlock]{ + Export-AllESPs $global:txtExportPath.Text + Set-ObjectGrid + Write-Status "" + }) + + $script:exportParams.Add("ExportSelectedScript", [ScriptBlock]{ + Export-SelectedESP $global:txtExportPath.Text + Set-ObjectGrid + Write-Status "" + }) + #Scriptblock that will perform the import all files + $script:importAll = [ScriptBlock]{ + Import-AllESPObjects $global:txtExportPath.Text + Set-ObjectGrid + } + + #Scriptblock that will perform the import of selected files + $script:importSelected = [ScriptBlock]{ + Import-ESPObjects $global:lstFiles.ItemsSource -Selected + Set-ObjectGrid + } + + #Scriptblock that will read json files + $script:getImportFiles = [ScriptBlock]{ + Show-FileListBox + $global:lstFiles.ItemsSource = @(Get-JsonFileObjects $global:txtImportPath.Text -Exclude "*_Settings.json") + } + + Add-DefaultObjectButtons -export ([scriptblock]{Show-DefaultExportGrid @script:exportParams}) -import ([scriptblock]{Show-DefaultImportGrid -ImportAll $script:importAll -ImportSelected $script:importSelected -GetFiles $script:getImportFiles}) -copy ([scriptblock]{Copy-ESP}) +} + +function Get-ESPObjects +{ + Get-GraphObjects -Url "/deviceManagement/deviceEnrollmentConfigurations" +} + +function Export-AllESPs +{ + param($path = "$env:Temp") + + if(-not (Test-Path $path)) { mkdir -Path $path -Force -ErrorAction SilentlyContinue | Out-Null } + + if(Test-Path $path) + { + foreach($objTmp in ($global:dgObjects.ItemsSource)) + { + Export-SingleESP $objTmp.Object $path + } + } +} + +function Export-SelectedESP +{ + param($path = "$env:Temp") + + Export-SingleESP $global:dgObjects.SelectedItem.Object $path +} + +function Export-SingleESP +{ + param($psObj, $path = "$env:Temp") + + if(-not $psObj) { return } + + if($global:runningBulkExport -ne $true) + { + if($global:chkAddCompanyName.IsChecked) { $path = Join-Path $path $global:organization.displayName } + if($global:chkAddObjectType.IsChecked) { $path = Join-Path $path (Get-ESPFolderName) } + } + + if(-not (Test-Path $path)) { mkdir -Path $path -Force -ErrorAction SilentlyContinue | Out-Null } + + if(Test-Path $path) + { + Write-Status "Export $($psObj.displayName)" + $obj = Invoke-GraphRequest -Url "/deviceManagement/deviceEnrollmentConfigurations/$($psObj.id)" #?`$expand=assignments" + if($obj) + { + if($obj.id -like "*_default*") + { + $idx = $obj.id.ToLower().IndexOf("_default") + $baseName = "Default_" + $obj.id.SubString($idx + "_default".Length) + } + else + { + # ?`$expand=assignments is not working so get assignments + $assignments = Invoke-GraphRequest -Url "/deviceManagement/deviceEnrollmentConfigurations/$($obj.id)/assignments" + if($assignments.value) + { + $obj | Add-Member -NotePropertyName "assignments" -NotePropertyValue $assignments.value + } + $baseName = Remove-InvalidFileNameChars $obj.displayName + } + $fileName = "$path\$baseName.json" + ConvertTo-Json $obj -Depth 5 | Out-File $fileName -Force + + Add-MigrationInfo $obj.assignments + } + $global:exportedObjects++ + } +} + +function Copy-ESP +{ + if(-not $dgObjects.SelectedItem) + { + [System.Windows.MessageBox]::Show("No object selected`n`nSelect enrollment status page item you want to copy", "Error", "OK", "Error") | Out-Null + return + } + + if($dgObjects.SelectedItem.Object.id -like "*_default*") + { + [System.Windows.MessageBox]::Show("You cannot copy default items`n`nSelect custom entrollment status page item", "Error", "OK", "Error") | Out-Null + return + } + + $ret = Show-InputDialog "Copy enrollment status page" "Select name for the new object" "$($dgObjects.SelectedItem.displayName) - Copy" + + if($ret) + { + # Export profile + Write-Status "Export $($dgObjects.SelectedItem.displayName)" + # Convert to Json and back to clone the object + $obj = ConvertTo-Json $dgObjects.SelectedItem.Object -Depth 5 | ConvertFrom-Json + if($obj) + { + # Import new profile + $obj.displayName = $ret + Import-ESP $obj | Out-Null + + $dgObjects.ItemsSource = @(Get-ESPObjects) + } + Write-Status "" + } + $dgObjects.Focus() +} + +function Import-ESP +{ + param($obj) + + if($obj.id -like "*_default*") + { + Write-Status "Update $($obj.displayName)" + + Invoke-GraphRequest -Url "/deviceManagement/deviceEnrollmentConfigurations/$($obj.id)" -Content (ConvertTo-Json $obj -Depth 5) -HttpMethod PATCH + } + else + { + Write-Status "Import $($obj.displayName)" + + Invoke-GraphRequest -Url "/deviceManagement/deviceEnrollmentConfigurations" -Content (ConvertTo-Json $obj -Depth 5) -HttpMethod POST + } +} + +function Import-AllESPObjects +{ + param($path = "$env:Temp") + + Import-ESPObjects (Get-JsonFileObjects $path) +} + +function Import-ESPObjects +{ + param( + $Objects, + + [switch] + $Selected + ) + + Write-Status "Import enrollment status page" + + foreach($obj in $Objects) + { + if($Selected -and $obj.Selected -ne $true) { continue } + + if($obj.Object.id -like "*_default*") + { + $idx = $obj.Object.id.ToLower().IndexOf("_default") + $extInfo = " ($($obj.Object.id.SubString($idx + "_default".Length)))" + } + else + { + $extInfo = "" + } + + Write-Log "Import Enrollment Status Page: $($obj.Object.displayName)$extInfo" + + $assignments = Get-GraphAssignmentsObject $obj.Object ($obj.FileInfo.DirectoryName + "\" + $obj.FileInfo.BaseName + "_assignments.json") + + $response = Import-ESP $obj.Object + + if($response) + { + $global:importedObjects++ + Import-GraphAssignments $assignments "enrollmentConfigurationAssignments" "/deviceManagement/deviceEnrollmentConfigurations/$($response.Id)/assign" + } + } + + $dgObjects.ItemsSource = @(Get-ESPObjects) + Write-Status "" +} \ No newline at end of file diff --git a/Extensions/GroupPolicy.psm1 b/Extensions/GroupPolicy.psm1 new file mode 100644 index 0000000..c61b7e4 --- /dev/null +++ b/Extensions/GroupPolicy.psm1 @@ -0,0 +1,322 @@ +######################################################## +# +# Common module functions +# +######################################################## +function Add-ModuleMenuItems +{ + Add-MenuItem (New-Object PSObject -Property @{ + Title = (Get-GPOSettingName) + MenuID = "IntuneGraphAPI" + Script = [ScriptBlock]{Get-GPOSettings} + }) +} + +function Get-SupportedImportObjects +{ + $global:importObjects += (New-Object PSObject -Property @{ + Selected = $true + Title = (Get-GPOSettingName) + Script = [ScriptBlock]{ + param($rootFolder) + + Write-Status "Import all administrative templates" + Import-AllGPOSettingObjects (Join-Path $rootFolder (Get-GPOSettingFolderName)) + } + }) +} + +function Get-SupportedExportObjects +{ + $global:exportObjects += (New-Object PSObject -Property @{ + Selected = $true + Title = (Get-GPOSettingName) + Script = [ScriptBlock]{ + param($rootFolder) + + Write-Status "Export all administrative templates" + Get-GPOSettingObjects | ForEach-Object { Export-SingleGPOSetting $PSItem.Object (Join-Path $rootFolder (Get-GPOSettingFolderName)) } + } + }) +} + +function Export-AllObjects +{ + param($addObjectSubfolder) + + $subFolder = "" + if($addObjectSubfolder) { $subFolder = Get-GPOSettingFolderName } +} + +######################################################## +# +# Object specific functions +# +######################################################## +function Get-GPOSettingName +{ + return "Administrative Templates" +} + + +function Get-GPOSettingFolderName +{ + return "AdministrativeTemplates" +} + +function Get-GPOSettings +{ + Write-Status "Loading administrative templates" + $dgObjects.ItemsSource = @(Get-GPOSettingObjects) + + #Scriptblocks that will perform the export tasks. empty by default + $script:exportParams = @{} + $script:exportParams.Add("ExportAllScript", [ScriptBlock]{ + Export-AllGPOSettings $global:txtExportPath.Text + Set-ObjectGrid + Write-Status "" + }) + + $script:exportParams.Add("ExportSelectedScript", [ScriptBlock]{ + Export-SelectedGPOSetting $global:txtExportPath.Text + Set-ObjectGrid + Write-Status "" + }) + + #Scriptblock that will perform the import all files + $script:importAll = [ScriptBlock]{ + Import-AllGPOSettingObjects $global:txtImportPath.Text + Set-ObjectGrid + } + + #Scriptblock that will perform the import of selected files + $script:importSelected = [ScriptBlock]{ + Import-GPOSettingObjects $global:lstFiles.ItemsSource -Selected + Set-ObjectGrid + } + + #Scriptblock that will read json files + $script:getImportFiles = [ScriptBlock]{ + Show-FileListBox + $global:lstFiles.ItemsSource = @(Get-JsonFileObjects $global:txtImportPath.Text -Exclude "*_Settings.json") + } + + Add-DefaultObjectButtons -export ([scriptblock]{Show-DefaultExportGrid @script:exportParams}) -import ([scriptblock]{Show-DefaultImportGrid -ImportAll $script:importAll -ImportSelected $script:importSelected -GetFiles $script:getImportFiles}) -copy ([scriptblock]{Copy-GPOSetting}) +} + +function Get-GPOSettingObjects +{ + Get-GraphObjects -Url "/deviceManagement/groupPolicyConfigurations" +} + +function Export-AllGPOSettings +{ + param($path = "$env:Temp") + + if(-not (Test-Path $path)) { mkdir -Path $path -Force -ErrorAction SilentlyContinue | Out-Null } + + if(Test-Path $path) + { + foreach($objTmp in ($global:dgObjects.ItemsSource)) + { + Export-SingleGPOSetting $objTmp.Object $path + } + } +} + +function Export-SelectedGPOSetting +{ + param($path = "$env:Temp") + + Export-SingleGPOSetting $global:dgObjects.SelectedItem.Object $path +} + +function Get-GPOObjectSettings +{ + param($GPOObj) + + $gpoSettings = @() + + # Get all configured policies in the Administrative Templates profile + $GPODefinitionValues = Invoke-GraphRequest -Url "/deviceManagement/groupPolicyConfigurations/$($GPOObj.id)/definitionValues?`$expand=definition" + foreach($definitionValue in $GPODefinitionValues.value) + { + # Get presentation values for the current settings (with presentation object included) + $presentationValues = Invoke-GraphRequest -Url "/deviceManagement/groupPolicyConfigurations/$($GPOObj.id)/definitionValues/$($definitionValue.id)/presentationValues?`$expand=presentation" + + # Set base policy settings + $obj = @{ + "enabled" = $definitionValue.enabled + "definition@odata.bind" = "$($global:graphURL)/deviceManagement/groupPolicyDefinitions('$($definitionValue.definition.id)')" + } + + if($presentationValues.value) + { + # Policy presentation values set e.g. a drop down list, check box, text box etc. + $obj.presentationValues = @() + + $presentations = $null + foreach ($presentationValue in $presentationValues.value) + { + # Add presentation@odata.bind property that links the value to the presentation object + $presentationValue | Add-Member -MemberType NoteProperty -Name "presentation@odata.bind" -Value "$($global:graphURL)/deviceManagement/groupPolicyDefinitions('$($definitionValue.definition.id)')/presentations('$($presentationValue.presentation.id)')" + + #Remove presentation object so it is not included in the export + Remove-ObjectProperty $presentationValue "presentation" + + #Optional removes. Import will igonre them + Remove-ObjectProperty $presentationValue "id" + Remove-ObjectProperty $presentationValue "lastModifiedDateTime" + Remove-ObjectProperty $presentationValue "createdDateTime" + + # Add presentation value to the list + $obj.presentationValues += $presentationValue + } + } + $gpoSettings += $obj + } + $gpoSettings +} + +function Export-SingleGPOSetting +{ + param($psObj, $path = "$env:Temp") + + if(-not $psObj) { return } + + if($global:runningBulkExport -ne $true) + { + if($global:chkAddCompanyName.IsChecked) { $path = Join-Path $path $global:organization.displayName } + if($global:chkAddObjectType.IsChecked) { $path = Join-Path $path (Get-GPOSettingFolderName) } + } + + if(-not (Test-Path $path)) { mkdir -Path $path -Force -ErrorAction SilentlyContinue | Out-Null } + + if(Test-Path $path) + { + Write-Status "Export $($psObj.displayName)" + $obj = Invoke-GraphRequest -Url "deviceManagement/groupPolicyConfigurations/$($psObj.Id)?`$expand=assignments" + + if($obj) + { + # Save Administrative Templates profile + ConvertTo-Json $obj -Depth 5 | Out-File "$path\$((Remove-InvalidFileNameChars $obj.displayName)).json" -Force + + # Collect and save all the settings of the Administrative Templates profile + $gpoSettings = Get-GPOObjectSettings $obj + ConvertTo-Json $gpoSettings -Depth 5 | Out-File "$path\$($obj.displayName)_Settings.json" -Force + + # Export assignment info + Add-MigrationInfo $obj.assignments + } + $global:exportedObjects++ + } +} + +function Copy-GPOSetting +{ + if(-not $dgObjects.SelectedItem) + { + [System.Windows.MessageBox]::Show("No object selected`n`nSelect administrative templates profile you want to copy", "Error", "OK", "Error") + return + } + + $ret = Show-InputDialog "Copy administrative template" "Select name for the new profile" "$($dgObjects.SelectedItem.displayName) - Copy" + + if($ret) + { + # Export profile + Write-Status "Export $($dgObjects.SelectedItem.displayName)" + # Convert to Json and back to clone the object + $obj = ConvertTo-Json $dgObjects.SelectedItem.Object -Depth 5 | ConvertFrom-Json + if($obj) + { + # Get the settings of the profile + $gpoSettings = Get-GPOObjectSettings $obj + + # Import the new profile + $obj.displayName = $ret + Import-GPOSetting $obj $gpoSettings | Out-Null + + #Reload objects + $dgObjects.ItemsSource = @(Get-GPOSettingObjects) + } + Write-Status "" + } + $dgObjects.Focus() +} + +function Import-GPOSetting +{ + param($obj, $settings) + + Write-Status "Import $($obj.displayName)" + + # Import Administrative Template profile + $response = Invoke-GraphRequest -Url "/deviceManagement/groupPolicyConfigurations" -Content (ConvertTo-Json $obj -Depth 5) -HttpMethod POST + + if($response) + { + foreach($setting in $settings) + { + # Import each setting for the Administrative Template profile + $response2 = Invoke-GraphRequest -Url "/deviceManagement/groupPolicyConfigurations/$($response.id)/definitionValues" -Content (ConvertTo-Json $setting -Depth 5) -HttpMethod POST + } + } + + $response +} + +function Import-AllGPOSettingObjects +{ + param($path = "$env:Temp") + + # Read json files and import all objects + # Note: Each json file must match the object type being imported + Import-GPOSettingObjects (Get-JsonFileObjects $path) +} + +function Import-GPOSettingObjects +{ + param( + $Objects, + + [switch] + $Selected + ) + + Write-Status "Import administrative template profile" + + foreach($obj in $objects) + { + if($Selected -and $obj.Selected -ne $true) { continue } + + Write-Log "Import Administrative Template: $($obj.Object.displayName)" + + $gpoSettings = $null + + # Load settings from the _settings.json file + $settingsFile = ($obj.FileInfo.DirectoryName + "\" + $obj.FileInfo.BaseName + "_settings.json") + if(Test-Path $settingsFile) + { + $gpoSettings = (ConvertFrom-Json (Get-Content $settingsFile -Raw)) + } + + # Get assignment settings + $assignments = Get-GraphAssignmentsObject $obj.Object ($obj.FileInfo.DirectoryName + "\" + $obj.FileInfo.BaseName + "_assignments.json") + + # Import Administrative Template object + $response = Import-GPOSetting $obj.Object $gpoSettings + + if($response) + { + $global:importedObjects++ + # Import assignments + Import-GraphAssignments $assignments "assignments" "/deviceManagement/groupPolicyConfigurations/$($response.Id)/assign" + } + } + + #Reload list of objects + $dgObjects.ItemsSource = @(Get-GPOSettingObjects) + Write-Status "" +} \ No newline at end of file diff --git a/Extensions/MDM_MAM.psm1 b/Extensions/MDM_MAM.psm1 new file mode 100644 index 0000000..3b80ab5 --- /dev/null +++ b/Extensions/MDM_MAM.psm1 @@ -0,0 +1,215 @@ +######################################################## +# +# Common module functions +# +######################################################## +function Add-ModuleMenuItems +{ + Add-MenuItem (New-Object PSObject -Property @{ + Title = (Get-MDMMAMName) + MenuID = "IntuneGraphAPI" + Script = [ScriptBlock]{Get-MDMMAM} + }) +} + +function Get-SupportedImportObjects +{ + $global:importObjects += (New-Object PSObject -Property @{ + Selected = $true + Title = (Get-MDMMAMName) + Script = [ScriptBlock]{ + param($rootFolder) + + Write-Status "Import all MDM/MAM setting" + Import-AllMDMMAMObjects (Join-Path $rootFolder (Get-MDMMAMFolderName)) + } + }) +} + +function Get-SupportedExportObjects +{ + $global:exportObjects += (New-Object PSObject -Property @{ + Selected = $true + Title = (Get-MDMMAMName) + Script = [ScriptBlock]{ + param($rootFolder) + + Write-Status "Export all MDM/MAM settings" + Get-MDMMAMObjects | ForEach-Object { Export-SingleMDMMAM $PSItem.Object (Join-Path $rootFolder (Get-MDMMAMFolderName)) } + } + }) +} + +function Export-AllObjects +{ + param($addObjectSubfolder) + + $subFolder = "" + if($addObjectSubfolder) { $subFolder = Get-MDMMAMFolderName } +} + +######################################################## +# +# Object specific functions +# +######################################################## +function Get-MDMMAMName +{ + return "MDM/MAM" +} + +function Get-MDMMAMFolderName +{ + return "MDMMAM" +} + +function Get-MDMMAM +{ + Write-Status "Loading MDM/MAM object" + $dgObjects.ItemsSource = @(Get-MDMMAMObjects) + + #Scriptblocks that will perform the export tasks. empty by default + $script:exportParams = @{} + $script:exportParams.Add("ExportAllScript", [ScriptBlock]{ + Export-AllMDMMAM $global:txtExportPath.Text + Set-ObjectGrid + Write-Status "" + }) + + $script:exportParams.Add("ExportSelectedScript", [ScriptBlock]{ + Export-SelectedMDMMAM $global:txtExportPath.Text + Set-ObjectGrid + Write-Status "" + }) + #Scriptblock that will perform the import all files + $script:importAll = [ScriptBlock]{ + Import-AllMDMMAMObjects $global:txtImportPath.Text + Set-ObjectGrid + } + + #Scriptblock that will perform the import of selected files + $script:importSelected = [ScriptBlock]{ + Import-MDMMAMObjects $global:lstFiles.ItemsSource -Selected + Set-ObjectGrid + } + + #Scriptblock that will read json files + $script:getImportFiles = [ScriptBlock]{ + Show-FileListBox + $global:lstFiles.ItemsSource = @(Get-JsonFileObjects $global:txtImportPath.Text -Exclude "*_Settings.json") + } + + Add-DefaultObjectButtons -export ([scriptblock]{Show-DefaultExportGrid @script:exportParams}) -import ([scriptblock]{Show-DefaultImportGrid -ImportAll $script:importAll -ImportSelected $script:importSelected -GetFiles $script:getImportFiles}) +} + +function Get-MDMMAMObjects +{ + Get-AzureNativeObjects "MdmApplications" -property @('appDisplayName') +} + +function Export-AllMDMMAM +{ + param($path = "$env:Temp") + + if(-not (Test-Path $path)) { mkdir -Path $path -Force -ErrorAction SilentlyContinue | Out-Null } + + if(Test-Path $path) + { + foreach($objTmp in ($global:dgObjects.ItemsSource)) + { + Export-SingleMDMMAM $objTmp.Object $path + } + } +} + +function Export-SelectedMDMMAM +{ + param($path = "$env:Temp") + + Export-SingleMDMMAM $global:dgObjects.SelectedItem.Object $path +} + +function Export-SingleMDMMAM +{ + param($psObj, $path = "$env:Temp") + + if(-not $psObj) { return } + + if($global:runningBulkExport -ne $true) + { + if($global:chkAddCompanyName.IsChecked) { $path = Join-Path $path $global:organization.displayName } + if($global:chkAddObjectType.IsChecked) { $path = Join-Path $path (Get-MDMMAMFolderName) } + } + + if(-not (Test-Path $path)) { mkdir -Path $path -Force -ErrorAction SilentlyContinue | Out-Null } + + if(Test-Path $path) + { + Write-Status "Export $($psObj.appDisplayName)" + + $obj = Invoke-AzureNativeRequest "MdmApplications/$($psObj.objectId)" + + if($obj) + { + $fileName = "$path\$((Remove-InvalidFileNameChars $obj.appDisplayName)).json" + ConvertTo-Json $obj -Depth 5 | Out-File $fileName -Force + } + + if($obj.mdmAppliesToGroups) + { + $obj.mdmAppliesToGroups | ForEach-Object { Add-GroupMigrationObject $PSItem.objectId } + } + + if($obj.mamAppliesToGroups) + { + $obj.mamAppliesToGroups | ForEach-Object { Add-GroupMigrationObject $PSItem.objectId } + } + + $global:exportedObjects++ + } +} + +function Import-MDMMAM +{ + param($obj) + + $argStr = "?" + if($obj.enrollmentUrl) { $argStr += "mdmAppliesToChanged=true" } + else{ $argStr += "mdmAppliesToChanged=false" } + if($obj.mamEnrollmentUrl) { $argStr += "&mamAppliesToChanged=true" } + else{ $argStr += "&mamAppliesToChanged=false" } + + $response = Invoke-AzureNativeRequest "MdmApplications/$($obj.objectId)$argStr" -Method PUT -Body (Update-JsonForEnvironment (ConvertTo-Json $obj -Depth 5)) +} + +function Import-AllMDMMAMObjects +{ + param($path = "$env:Temp") + + Import-MDMMAMObjects (Get-JsonFileObjects $path) +} + +function Import-MDMMAMObjects +{ + param( + $Objects, + + [switch] + $Selected + ) + + Write-Status "Import MDM/MAM settings" + + foreach($obj in $objects) + { + if($Selected -and $obj.Selected -ne $true) { continue } + + Write-Log "Import MDM/MAM app settings: $($obj.Object.appDisplayName)" + + Import-MDMMAM $obj.Object + + # No assignments for MDM/MAM + } + $dgObjects.ItemsSource = @(Get-MDMMAMObjects) + Write-Status "" +} \ No newline at end of file diff --git a/Extensions/MSGraphIntune.psm1 b/Extensions/MSGraphIntune.psm1 new file mode 100644 index 0000000..a1c66ed --- /dev/null +++ b/Extensions/MSGraphIntune.psm1 @@ -0,0 +1,1438 @@ +function Invoke-InitializeModule +{ + $module = Get-Module -Name Microsoft.Graph.Intune -ListAvailable + if(-not $module) + { + $ret = [System.Windows.MessageBox]::Show("Intune PowerShell module not found!`n`nDo you want to install it?`n`nYes = Install intune module (Requires admin or it will fail)`nNo = Contune without module (Nothing will work)`nCancel = Quit", "Error", "YesNoCancel", "Error") + if($ret -eq "Yes") + { + try + { + Install-Module -Name Microsoft.Graph.Intune -Force -ErrorAction SilentlyContinue + } + catch {} + if(-not (Get-Module -Name Microsoft.Graph.Intune)) + { + [System.Windows.MessageBox]::Show("Failed to install Intune PowerShell module!`n`nRestart this as admin and try again`nor`nStart PowerShell as admin and run:`nInstall-Module -Name Microsoft.Graph.Intune", "Error", "OK", "Error") + exit + } + } + elseif($ret -eq "Cancel") + { + exit + } + } + + if(-not $global:authentication) + { + if((Get-Command Connect-MSGraph)) + { + $global:authentication = Connect-MSGraph -PassThru + } + } + + $global:graphURL = "https://graph.microsoft.com/beta" + +# Add settings + $global:appSettingSections += (New-Object PSObject -Property @{ + Title = "Intune" + Id = "IntuneAzure" + Values = @() + }) + + Add-SettingsObject (New-Object PSObject -Property @{ + Title = "Root folder" + Key = "IntuneRootFolder" + Type = "Folder" + }) "IntuneAzure" + + Add-SettingsObject (New-Object PSObject -Property @{ + Title = "App packages folder" + Key = "IntuneAppPackages" + Type = "Folder" + }) "IntuneAzure" + + Add-SettingsObject (New-Object PSObject -Property @{ + Title = "Add object type" + Key = "AddObjectType" + Type = "Boolean" + DefaultValue = $true + }) "IntuneAzure" + + Add-SettingsObject (New-Object PSObject -Property @{ + Title = "Add company name" + Key = "AddCompanyName" + Type = "Boolean" + DefaultValue = $true + }) "IntuneAzure" + + Add-SettingsObject (New-Object PSObject -Property @{ + Title = "Export Assignments" + Key = "ExportIntuneAssignments" + Type = "Boolean" + DefaultValue = $true + }) "IntuneAzure" + + Add-SettingsObject (New-Object PSObject -Property @{ + Title = "Create groups" + Key = "CreateIntuneGroupOnImport" + Type = "Boolean" + DefaultValue = $true + }) "IntuneAzure" + + Add-SettingsObject (New-Object PSObject -Property @{ + Title = "Convert synced groups" + Key = "ConvertIntuneSyncedGroupOnImport" + Type = "Boolean" + DefaultValue = $true + }) "IntuneAzure" + + Add-SettingsObject (New-Object PSObject -Property @{ + Title = "Import Assignments" + Key = "ImportAssignments" + Type = "Boolean" + DefaultValue = $true + }) "IntuneAzure" + + + #Add menu group and items + Add-MenuSection (New-Object PSObject -Property @{ Title = "Intune/Azure Objects"; ID="IntuneGraphAPI"; Order = 10}) + Add-MenuSection (New-Object PSObject -Property @{ Title = "Intune/Azure Management"; ID="IntuneGraphAPIEX"; Order = 20}) + + # Add default menu items + Add-MenuItem (New-Object PSObject -Property @{ + Title = 'Bulk Import' + MenuID = "IntuneGraphAPIEX" + Script = [ScriptBlock]{ Show-ImportAllForm } + }) + + # Add default menu items + Add-MenuItem (New-Object PSObject -Property @{ + Title = 'Bulk Export' + MenuID = "IntuneGraphAPIEX" + Script = [ScriptBlock]{ Show-ExportAllForm } + }) + + $global:Me = Invoke-GraphRequest "ME" + $global:Organization = (Invoke-GraphRequest "Organization").Value + + $global:UpdateJsonForMigration = $true +} + +function Show-ExportAllForm +{ + param($Extension) + +$xmlStr = @" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + $($Extension.Xaml) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + $($Extension.Xaml) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + $($Extension.Xaml) + + + + + + + + + + + + + + + + + + + + $($Extension.Xaml) + + +