- Introduction
- Usage
- Backup
- Restore
- Creating Repo
- Azure AD App Reg
- Automating Script
- Azure Automation Runbook
Update – 02/08/2023
Added support for GitLab
Update – 07/03/2023
Launched Intunebackup.com to provide a web front end to the script running in an Azure Automation runbook (More info here)
Update – 05/03/2023
Version 5.0 released, changelog:
- Added extra parameters to trigger backup or restore via ID or Name without GUI at single policy level
- Added support for webhook
- Replaced write-host with write-output for use with Azure Automation Runbook
- Added parameter for filename to skip grid-view on automated restore
- Bypass script check when running on webhook
- Github fix to cope with large files
Update – 24/02/2023
Version 4.0 released with a significant performance improvement
Update – 16/02/2023
Version 3.0 now released with added support for Azure DevOps Repo as well as GitHub, can be hard-coded, or via launch parameters
Update – 05/01/2023
Version 2.0 just released adding support for:
- Policy Sets
- Enrollment Configuration Policies
- Device Categories
- Device Filters
- Branding Profiles
- Admin Approvals
- Intune Terms
- Custom roles
Introduction
If you are working with virtual machines, you would normally grab a snapshot before making any changes, so why can’t we do the same with Intune/AAD config?
I have previously looked at backing up your Intune environment with an automated big-bang approach (post here), which works well, but:
- Restoring isn’t as easy as I would like
- It’s a big-bang restore, everything is restored
- The names are the same so need to dig around to find which is the new one
- Versioning isn’t particularly straight forward
- It uses the (whilst excellent) backup and restore module which relies on the Azure AD module (due for deprecation shortly)
With that in mind, I set about creating my own backup and restore script (yes, script, not module, it’s a single file) which includes:
- Selective backup, you can pick what policies, Conditional Access, AAD Groups, Proactive Remediations, Script etc. you want to backup (via GUI)
- A few extra backup options over the previous script
- Backup to a single file
- Backup to a Private Github Repo to give you all of the goodness that brings
- Backups include Commit messages so you know why you made them
- You can pick which restore point to use
- You can pick what to restore from the selected restore point
- Whatever you restore will have “-restore-DATE” at the start of the name so you know what it is and when it was restored (date of restore, not date of backup)!
- All written using the new Graph SDK
- Everything you need to set is in parameters to be able to use across multiple customers
So, after many hours of work, here it is. As usual you can grab it from GitHub Here or the PSGallery:
Install-Script -Name intune-backup-restore-withgui
First I’ll run through how the script works and then below how to configure the Github Repo, secret and automate it.
Usage
After configuring your Github repo, you need to run the script with the parameters required (available in get-help too)
intune-backup-restore-withgui.ps1 -type backup -selected some -reponame intunebackups -ownername my-github-account -token my-github-token
Type can be “backup” or “restore” depending on what you are doing
Selected can be “all” to simply grab or restore everything, or anything else will show the gui (it’s an if/else query)
Backup
When running in backup mode, you will see a GUI asking what you want to backup:
You can filter and select multiple via Ctrl/Shift click
Clicking Ok will write them to a single json file and upload it to your specified Github repo including the current date/time to reference when restoring
Restore
When running in Restore mode, you will first see a GUI asking which backup you want to restore from:
After selecting the backup, you will get another GUI showing the content within that backup where you can select what to restore
Click Ok and you’ll see the policies in your tenant with “Restored” at the start of the name
Creating Github Repo and Token
Create a repository, public will also work, but private probably makes more sense for backups
Now we need to create the token, in GitHub navigate to Settings (in the top right menu) – Developer Settings (at the bottom)
I’m using a Personal Access Token, but the API commands are all compatible with a GitHub App if you would prefer
Generate a new classic token and grab the output, it only needs Repo access
Azure AD App Registration
To use in Automated mode, you’ll need to create a secret:
Navigate to Azure AD and click App Registrations, then create a new Application
Give your application a name you will recognise and then select either Single Tenant or Multi-Tenant depending on your requirements, then click Register
Grab the Application (Client ID) from the Overview screen
Now click Authentication
Click Add Platform
You want Mobile or Desktop Application
Tick the 3 boxes and click Configure
Next, select API Permissions and click Add a Permission
Select Microsoft Graph
Select Application Permissions
Add these:
- DeviceManagementApps.ReadWrite.All
- DeviceManagementConfiguration.ReadWrite.All
- DeviceManagementManagedDevices.ReadWrite.All
- Domain.Read.All
- Group.ReadWrite.All
- Organization.Read.All
- Policy.ReadWrite.ConditionalAccess
- RoleManagement.ReadWrite.Directory
- DeviceManagementServiceConfig.ReadWrite.All
You’ll see it’s still not finished, you need to click Grant admin consent
Last step here, click Certificates & secrets
Add a new secret
Add a name you’ll remember and set an expiry (I’m using 24 months because I’m lazy)
Copy the Secret Value somewhere safe, it won’t re-display it and if you lose it you’ll need to create a new one
Automating – Script Amendment
As you can see, normally this runs with a GUI and user authentication, but that won’t work in an automated task.
To automate, change this line to “yes”
############################################################
$automated = "no"
############################################################
Then populate the variables:
$selected = "all"
$reponame = "YOUR_REPONAME_HERE"
$ownername = "YOUR_OWNER_NAME_FOR_REPO"
$token = "YOUR_GITHUB_TOKEN"
$clientid = "YOUR_AAD_REG_ID"
$clientsecret = "YOUR_CLIENT_SECRET"
$tenant = "TENANT_ID"
Automation Deployment
First, let’s create the Azure Automation Account:
Search for Azure Automation in your Azure Portal and create the account:
Now you’ll need to publish some PowerShell modules to the runbook (watch the versions on the Graph modules, it needs the non-preview ones):
For each module, click this button:
https://www.powershellgallery.com/packages/PackageManagement/1.4.8.1
https://www.powershellgallery.com/packages/Microsoft.Graph.Authentication/1.19.0
https://www.powershellgallery.com/packages/Microsoft.Graph.Devices.CorporateManagement/1.19.0
https://www.powershellgallery.com/packages/Microsoft.Graph.Groups/1.19.0
https://www.powershellgallery.com/packages/Microsoft.Graph.DeviceManagement/1.19.0
https://www.powershellgallery.com/packages/Microsoft.Graph.Identity.SignIns/1.19.0
Create a new Runbook
Add the PowerShell code you’ve amended
Click Test Pane to make sure it’s worked
Now we need to publish it once it’s tested ok
Now we can add a schedule to run it weekly
That’s it, your basic weekly/daily/hourly backup is configured and will go direct to the Azure Storage Blob
If you want to get really clever, we can trigger a backup when a setting is changed within Intune using Event Hub. Follow my previous instructions here to create the event hub and link it to Intune and then use this logic app instead:
Select Event Hub as the trigger and use the connection string created before
For the action, find Azure Automation and set it to Create Job, then populate the details:
Then if any changes are made in Intune, you’ll see your Automation Job activate:
I am looking at the code and have found a variable that is unused – $EncodedText
I wondered if the line:
$pvalue.value = $unencoded
was supposed to be:
$pvalue.value = $EncodedText
Code as it is right now:
##Custom settings only for OMA-URI
##Remove settings which break Custom OMA-URI
if ($null -ne $policy.omaSettings) {
$policyconvert = $policy.omaSettings
$policyconvert = $policyconvert | Select-Object -Property * -ExcludeProperty secretReferenceValueId
foreach ($pvalue in $policyconvert) {
$unencoded = $pvalue.value
##Check if $unencoded is boolean
if ($unencoded -is [bool] -or $unencoded -is [int] -or $unencoded -is [int32] -or $unencoded -is [int64]) {
$unencoded = $unencoded.ToString().ToLower()
}
$EncodedText = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($unencoded))
$pvalue.value = $unencoded
}
$policy.omaSettings = @($policyconvert)
}
That’s an old variable which I replaced so it could cope with boolean values, you can just ignore it
Hi do i first need to connect to graph before i run the commands?
Yes, I’m not sure this module will work with the retirement of the AzureAD module though. You might want to look at my own script instead
This is just the tool I’ve been looking for! Thanks! Any idea why I’m only seeing AAD Groups when running a backup of everything? (-type backup -selected all)
Hi, that’s probably a permissions issue. If you are using an app reg, check it has the correct permissions and they are granted for the tenant
Hi Andrew, if i have a backup and restore set to my tenant but I want to restore the backup to another new tenant. What and where do I make changes to? I have tried to figure this out but i cant seem to find it. Thank you
Hi, the easiest way is to set the source tenant as your gold tenant, then deploy from there to the new one
Hey, you can ignore me, I was able to resolve this. Thank you again for this solution, it is a lifesaver.
how can i backup the results just to my computer ?
its still early morning, found my own answer:
earlier:
Hi Randy,
By default it only outputs to Azure or Github, but if you leave the PS Window open, you could output the raw json:
$policyjson | out-file c:\temp\mypolicies.json
Reply
tried this, but all variables were empty.
all i want to do is a backup to my computer.
Do i need to create the azure automation runbook in order to backup ?
or just an app registration / Global administrator login will suffice ?
sidenote, i made this to export all variables to json, might be usefull for people. (chatggpt generated)
# Set the threshold time to just before the script was run
$thresholdTime = [datetime]::ParseExact(“2024-04-04 11:45”, “yyyy-MM-dd HH:mm”, $null)
# Get all user-defined variables
$variables = Get-Variable | Where-Object { -not $_.Name.StartsWith(“_”) -and -not $_.Name.Contains(“::”) }
foreach ($variable in $variables) {
# Convert each variable to JSON, handling types as necessary
$contentToConvert = $variable.Value
if ($contentToConvert -is [System.Collections.IDictionary]) {
# Convert dictionaries to hashtable
$contentToConvert = @{}
foreach ($key in $variable.Value.Keys) {
$contentToConvert[$key] = $variable.Value[$key]
}
} elseif ($contentToConvert -is [System.Collections.IEnumerable] -and -not ($contentToConvert -is [string])) {
# Convert other enumerable types to arrays
$contentToConvert = @($variable.Value)
} else {
# Wrap non-enumerable, non-dictionary types as custom objects
$contentToConvert = [PSCustomObject]@{Value = $contentToConvert}
}
try {
# Convert the content to JSON
$jsonContent = $contentToConvert | ConvertTo-Json -Depth 5
} catch {
Write-Warning “Could not convert variable $($variable.Name) to JSON: $_”
continue
}
# Export to JSON file, naming the file after the variable
try {
$fileName = “.\” + $variable.Name + “.json”
$jsonContent | Out-File $fileName
} catch {
Write-Warning “Could not write variable $($variable.Name) to file: $_”
}
}
You’ll need to edit the script, remove the Git output parts and replace with an out-file onto your machine
Hello Andrew,
PS5 is the winner…
You can’t restore backups made by PS7 but PS5 / PS5 is working just fine…
thanks
Glad it’s working, I’ll do some more testing with PS7
I am not able to figure this out, i have followed all the steps in your article, but i keep getting:
Uploading to Github
Invoke-RestMethod : {“message”:”Not
Found”,”documentation_url”:”https://docs.github.com/rest/repos/contents#create-or-update-file-contents”}
At C:\Program Files\WindowsPowerShell\Scripts\intune-backup-restore-withgui.ps1:5766 char:2
+ (Invoke-RestMethod -Uri $uri -Method put -Headers @{‘Authorization’=’ …
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (System.Net.HttpWebRequest:HttpWebRequest) [Invoke-RestMethod], WebExc
eption
+ FullyQualifiedErrorId : WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeRestMethodCommand
intune-backup-restore-withgui.ps1 -type backup -select all -reponame -ownername -token
I am an writer etc on the affected GItHub Repo and my account is using the correct token.
Any idea what i’m doing wrong?
That’s normally something wrong with the GitHub details, double check the reponame and ownername
Hello Andrew,
thank you for your work…
I have just some issues with backup / restore…
First, just some error during the backup:
Line |
475 | Select-MgProfile -Name Beta
| ~~~~~~~~~~~~~~~~
| The term ‘Select-MgProfile’ is not recognized as a name of a cmdlet, function, script file, or executable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try
| again.
Despite that, backup proceeded without further errors. But during the test restore attempt of the Intune configuration profile from Github Repository:
Invoke-RestMethod: C:\Program Files\WindowsPowerShell\Scripts\intune-backup-restore-withgui.ps1:5868:31
Line |
5868 | … download = (Invoke-RestMethod -Uri $commitfilename2 -Method Get -Head …
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| The format of value ‘Accept: application/json’ is invalid.
Invoke-RestMethod: C:\Program Files\WindowsPowerShell\Scripts\intune-backup-restore-withgui.ps1:5869:46
Line |
5869 | … ecodedbackup = (Invoke-RestMethod -Uri $decodedbackupdownload -Method …
| ~~~~~~~~~~~~~~~~~~~~~~
| Cannot validate argument on parameter ‘Uri’. The argument is null or empty. Provide an argument that is not null or empty, and then try the command again.
0
InvalidOperation: C:\Program Files\WindowsPowerShell\Scripts\intune-backup-restore-withgui.ps1:5988:5
Line |
5988 | $value1 = ($profilelist3)[2]
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| Cannot index into a null array.
InvalidOperation: C:\Program Files\WindowsPowerShell\Scripts\intune-backup-restore-withgui.ps1:5989:5
Line |
5989 | $prid = ($profilelist3)[3]
| ~~~~~~~~~~~~~~~~~~~~~~~~~~
| Cannot index into a null array.
any idea what should I do to resolve it?
Hi,
The first error is usually if you have both V1 and V2 modules installed, it works, but uses V1 commands on V2.
Can you try changing the GitHub header to:
Accept: application/vnd.github+json
If that works, I’ll update the script
Invoke-RestMethod: C:\Install\X\xintune-backup-restore-withgui.ps1:5868
Line |
5868 | … download = (Invoke-RestMethod -Uri $commitfilename2 -Method Get -Head …
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| The format of value ‘Accept: application/vnd.github+json’ is invalid.
Invoke-RestMethod: C:\Install\X\xintune-backup-restore-withgui.ps1:5869
Line |
5869 | … ecodedbackup = (Invoke-RestMethod -Uri $decodedbackupdownload -Method …
| ~~~~~~~~~~~~~~~~~~~~~~
| Cannot validate argument on parameter ‘Uri’. The argument is null or empty. Provide an argument that is not null
| or empty, and then try the command again.
0
InvalidOperation: C:\Install\X\xintune-backup-restore-withgui.ps1:5988
Line |
5988 | $value1 = ($profilelist3)[2]
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| Cannot index into a null array.
InvalidOperation: C:\Install\X\xintune-backup-restore-withgui.ps1:5989
Line |
5989 | $prid = ($profilelist3)[3]
| ~~~~~~~~~~~~~~~~~~~~~~~~~~
| Cannot index into a null array.
I’ve just tested and it’s working ok for me with the original code. Are you using the older access token?
Personal access tokens (classic) created ad hoc just for this project… I followed your “how to” But since it’s working for you I’ am gonna do some digging at my end… Just not sure if I have a problem at the github or azure end…
Are you using PS7 or PS5 at the Azure side? Might be worth trying 5 if you’re on 7 currently, although it should work with both
Thanks, ill keep an eye out for your email
Ive set this up and im running the TEST in the runbook. I get the following output during the test but after a period of time it just seems to stop doing anything. I refresh the job streams and no new lines come up and the test never seems to complete.
I dont have many policies but so far this “TEST” has been running for 2 hours and nothing has moved past this point. What am i doing wrong? No errors seem to display either.
I can run a manual backup using the GUI fine but the automation doesn’t seem to want to work.
It’s an enrollment configuration
85fd1e7e-0e6d-41bd-887f-06f69ab36314_DefaultWindowsHelloForBusiness
It’s an AutoPilot ESP
It’s a WHfB Policy
It’s an enrollment configuration
91bbf654-048b-4f9d-a9ff-12b515263118
It’s a device filter
36d53cb6-5509-421d-8517-a4f7c1d54ef3
It’s a device filter
ead8b24e-ac7a-488a-baec-cfed41577c95
It’s a device filter
5bcd81e0-bd7f-494b-bbc6-93bc4c81c1f2
It’s a device filter
f199cbe2-f45d-4efa-8511-2c9330620063
It’s a device filter
ee05f163-1e98-4029-a13e-40a440f7abbf
Is there anything in the warning or error output in the automation runbook? Check your app reg permissions are correct as well
Nothing in the output no, only what I’ve pasted above are the last few lines but other than that no errors.
When you say app reg permissions your referring to these?
DeviceManagementApps.ReadWrite.All
DeviceManagementConfiguration.ReadWrite.All
DeviceManagementManagedDevices.ReadWrite.All
Domain.Read.All
Group.ReadWrite.All
Organization.Read.All
Policy.ReadWrite.ConditionalAccess
RoleManagement.ReadWrite.Directory
DeviceManagementServiceConfig.ReadWrite.All
I have all of these set as per your guide.
No warnings or errors in the Azure portal for the runbook? Have you passed through the Git credentials?
Im not sure where I would find that? Im brand new to Runbooks and Azure automation. Literally only set it up so I could automate my Intune backups.
Im using github for the repository and have put those values into the script in the runbook.
It must be connecting to my tenant to be able to pick this up and get the policys IDs so it must be connecting. Why it just randomly stops in the middle though is where im stuck
It’s an enrollment configuration
85fd1e7e-0e6d-41bd-887f-06f69ab36314_DefaultWindowsHelloForBusiness
It’s an AutoPilot ESP
It’s a WHfB Policy
It’s an enrollment configuration
91bbf654-048b-4f9d-a9ff-12b515263118
It’s a device filter
36d53cb6-5509-421d-8517-a4f7c1d54ef3
It’s a device filter
ead8b24e-ac7a-488a-baec-cfed41577c95
It’s a device filter
5bcd81e0-bd7f-494b-bbc6-93bc4c81c1f2
It’s a device filter
f199cbe2-f45d-4efa-8511-2c9330620063
It’s a device filter
ee05f163-1e98-4029-a13e-40a440f7abbf
I’ll drop you an email to get some screenshots
Hi, Thanks for this. After setting up everything and wanted to test restoring; using this: intune-backup-restore-withgui.ps1 -type restore -selected all -reponame *********-ownername ***** -token ghp_******************************
it only gives me the option to select the backup but doesnt open the backup to select the specific item i want to restore. I see a notification ‘do you want to restore assignments?’ and attempts to restore all
please is the script broken?
Hi, it is because you have -selected set to “All” which will restore everything. Change that to -selected some
oh! great… Thanks
additionally, the ‘Restore’ appears at the end of the name not at the start as your guides states.
You can turn that off if needed, change line 232 to No
Hello, the script is working as a charm!
But when we run it in a runbook it gets this error:
Thread failed to start. (Thread failed to start. (Exception of type ‘System.OutOfMemoryException’ was thrown.))
There is a memory limit on the azure sandbox of 400Mb so I guess the workerthread exceeds this. Is there any way to divide the automated backup into pieces or any other solution you can think of?
It is only one customer that this happens to and they have alot of configurations.
Thanks
That must be some environment! You could try running a hybrid runbook and pop a VM behind it to do the script. That should get around the restrictions.
Getting this error when running in Automation Account:
fd6ac55e-d57c-47b9-8803-f9c3eb3789cb
It’s a policy
System.Management.Automation.MethodException: Cannot convert argument “chars”, with value: “True”, for “GetBytes” to type “System.Char[]”: “Cannot convert value “True” to type “System.Char[]”. Error: “Invalid cast from ‘System.Boolean’ to ‘System.Char[]’.”” —> System.Management.Automation.PSInvalidCastException: Cannot convert value “True” to type “System.Char[]”. Error: “Invalid cast from ‘System.Boolean’ to ‘System.Char[]’.” —> System.InvalidCastException: Invalid cast from ‘System.Boolean’ to ‘System.Char[]’.
at System.Convert.DefaultToType(IConvertible value, Type targetType, IFormatProvider provider)
at System.Management.Automation.LanguagePrimitives.ConvertIConvertible(Object valueToConvert, Type resultType, Boolean recursion, PSObject originalValueToConvert, IFormatProvider formatProvider, TypeTable backupTable)
— End of inner exception stack trace —
at System.Management.Automation.LanguagePrimitives.ConvertIConvertible(Object valueToConvert, Type resultType, Boolean recursion, PSObject originalValueToConvert, IFormatProvider formatProvider, TypeTable backupTable)
at System.Management.Automation.LanguagePrimitives.ConvertCheckingForCustomConverter.Convert(Object valueToConvert, Type resultType, Boolean recursion, PSObject originalValueToConvert, IFormatProvider formatProvider, TypeTable backupTable)
at CallSite.Target(Closure , CallSite , Encoding , Object )
— End of inner exception stack trace —
at System.Management.Automation.ExceptionHandlingOps.ConvertToArgumentConversionException(Exception exception, String parameterName, Object argument, String method, Type toType)
at CallSite.Target(Closure , CallSite , Encoding , Object )
at System.Dynamic.UpdateDelegates.UpdateAndExecute2[T0,T1,TRet](CallSite site, T0 arg0, T1 arg1)
at System.Management.Automation.Interpreter.DynamicInstruction`3.Run(InterpretedFrame frame)
at System.Management.Automation.Interpreter.EnterTryCatchFinallyInstruction.Run(InterpretedFrame frame)
4b2d8e79-e50a-4a21-95ea-8cd7acd91179
It’s a policy
c3c07401-dc4e-4a10-b6ee-34ba2f8c2af3
It’s a policy
—
Then it continues to cycle through the policies but fails in the end:
An error occurred stopping transcription: The host is not currently transcribing.
The transcript error will happen in automation. The other error is probably pagination and shouldn’t cause any issues.
Have a look for any errors when exporting the JSON, it’s normally a permissions issue at the Git level
Hello Andrew,
We have created the backup account successfully using above details, but we are unable to identify where its creating its backup. As i am assuming it should be under Storage browser>Blob container>the name of your backup. But when i click it after running the runbook successfully getting no backup job created there. More details, we donot have anything currently in our intune tenant.
Hi, it will backup to whichever Git repo you tell it to use, either with a parameter or embedded in the script.
You could also use my intunebackup.com service if you want a better GUI
May be a weird ask here but going for it anyways.
Would it be a crazy ask to add GitLab as a repotype?
Good question, I’ll spin up a server and see how close their API is to the others.
The answer seems to be yes, just testing it now in dev
Version 6.1.0 now supports GitLab too, just use the -project parameter to send the project ID
Hi Andrew
If I wanted to save the backup to a folder in an existing repo, how would I specify that?
Thanks
Paul
Hi Paul,
I think if you set the repo to “reponame/foldername” that should pick it up
Thanks
Andrew
Hi Andrew,
What command would I use if I want to save the backup locally? Is that an option with this script, or are GitHub or Azure the only options for saving?
Thanks in advance,
Randy
Hi Randy,
By default it only outputs to Azure or Github, but if you leave the PS Window open, you could output the raw json:
$policyjson | out-file c:\temp\mypolicies.json
In this case, I used Azure DevOps. However, I used to manually execute a pull request after every commit. But when I stopped doing that and instead selected the commit as a backup directly, the error no longer occurred.
However, I’m getting the following error when backing up all of my Windows Autopatch configuration profiles (settings catalog):
Invoke-MgGraphRequest : Cannot convert ‘System.Object[]’ to the type ‘System.Uri’ required by parameter ‘Uri’.
Specified method is not supported.
At C:\tmp\intune-backup-restore-withgui.ps1:1965 char:57
+ … $nextsettings = (Invoke-MgGraphRequest -Uri $policynextlink -Method …
+ ~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidArgument: (:) [Invoke-MgGraphRequest], ParameterBindingException
+ FullyQualifiedErrorId : CannotConvertArgument,Microsoft.Graph.PowerShell.Authentication.Cmdlets.InvokeMgGraphReq
uest
It is the identical error for all Windows autopatch profiles
Ah, pagination again. I’ve just added a new version which should stop that error appearing (it was adding whitespace so cancelling the $null query)
Hi Andrew,
Thank you for your great work.
I always get the following error when trying to restore. Regardless of whether Win365 user settings, normal config profiles, etc.
ConvertFrom-Json : Invalid JSON primitive: .
At C:\tmp\intune-backup-restore-withgui.ps1:2824 char:34
+ $profilelist2 = $decodedbackup | ConvertFrom-Json
+ ~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [ConvertFrom-Json], ArgumentException
+ FullyQualifiedErrorId : System.ArgumentException,Microsoft.PowerShell.Commands.ConvertFromJsonCommand
Cannot index into a null array.
At C:\tmp\intune-backup-restore-withgui.ps1:2841 char:5
+ $value1 = ($profilelist3)[2]
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (:) [], RuntimeException
+ FullyQualifiedErrorId : NullArray
Do you have any idea what it could be?
Thanks very much!
Hi Florian,
Are you using GitHub or DevOps to store the backups? I’ll do some testing and see if I can replicate the error
Thanks for improving it further, I’m using in on my lab. Made some modifications to use azure storage.
Looks a lot better now yes. Only a few errors, and now it really qucikly displays the list of config to select for backup.
PS>TerminatingError(Invoke-MgGraphRequest): “GET https://graph.microsoft.com/beta/identity/conditionalAccess/policies
HTTP/1.1 403 Forbidden
PS>TerminatingError(Invoke-MgGraphRequest): “GET https://graph.microsoft.com/beta/deviceManagement/virtualEndpoint/provisioningPolicies
PS>TerminatingError(Invoke-MgGraphRequest): “GET https://graph.microsoft.com/beta/deviceManagement/virtualEndpoint/userSettings
I’m pretty sure I added the required rights, and I checked using graph explorer, the rights and queries, then compared with api permissions.
If I just select canced after the list of config is displayed, and then cancel on the backup path it does not seem to by happy about that:
At C:\Temp\intune-backup-restore-withgui-v211.ps1:2456 char:1
+ $profilesencoded =[Convert]::ToBase64String([Text.Encoding]::UTF8.Get …
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : ArgumentNullException
Exception calling “GetBytes” with “1” argument(s): “Array cannot be null.
Parameter name: chars”
At C:\Temp\intune-backup-restore-withgui-v211.ps1:2456 char:1
+ $profilesencoded =[Convert]::ToBase64String([Text.Encoding]::UTF8.Get …
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : ArgumentNullException
Ah, I can see the issues, I was missing some scopes for Win365 machines. The new version I’ve just added should sort that for you
Hello and sorry for late reply. I’m still getting errors in the logs before the gridview opens, such as:
PS>TerminatingError(Invoke-MgGraphRequest): “GET https://graph.microsoft.com/beta/identity/conditionalAccess/policies
HTTP/1.1 403 Forbidden
I can run the previous query in graph explorer successfully and I have granted the same rights .
Content-Type: application/json
{“error”:{“code”:”AccessDenied”,”message”:”You cannot perform the requested operation, required scopes are missing in the token.”,”innerError”:{“date”:”2023-02-08T07:26:06″,”request-id”:””,”client-request-id”:””}}}”
PS>TerminatingError(Invoke-MgGraphRequest): “GET https://graph.microsoft.com/beta/deviceManagement/virtualEndpoint/userSettings
HTTP/1.1 403 Forbidden
Content-Type: application/json
{“error”:{“code”:”accessDenied”,”message”:”Access is denied to the requested resource.”,”innerError”:{“date”:”2023-02-08T07:26:10″,”request-id”:””,”client-request-id”:””}}}”
PS>TerminatingError(Invoke-MgGraphRequest): “GET https://graph.microsoft.com/beta/deviceManagement/virtualEndpoint/provisioningPolicies
HTTP/1.1 403 Forbidden
Then in the gridview I select one settings catalog profile and get additional errors in the logs, however the json backup is created and uploaded to github:
It is the same guid that is repeated in below errors again and again but different configs:
Content-Type: application/json
{“error”:{“code”:”accessDenied”,”message”:”Access is denied to the requested resource.”,”innerError”:{“date”:”2023-02-08T07:26:10″,”request-id”:””,”client-request-id”:””}}}”
PS>TerminatingError(Invoke-MgGraphRequest): “GET https://graph.microsoft.com/beta/deviceManagement/intents/
HTTP/1.1 404 Not Found
Same for:
PS>TerminatingError(Invoke-MgGraphRequest): “GET https://graph.microsoft.com/Beta/deviceAppManagement/androidManagedAppProtections(”)
HTTP/1.1 400 Bad Request
PS>TerminatingError(Invoke-MgGraphRequest): “GET https://graph.microsoft.com/Beta/deviceAppManagement/iOSManagedAppProtections(”)
PS>TerminatingError(Invoke-MgGraphRequest): “GET https://graph.microsoft.com/beta/identity/conditionalAccess/policies/
PS>TerminatingError(Invoke-MgGraphRequest): “GET https://graph.microsoft.com/beta/deviceManagement/devicehealthscripts/
PS>TerminatingError(Invoke-MgGraphRequest): “GET https://graph.microsoft.com/Beta/deviceAppManagement/mobileApps/
https://graph.microsoft.com/beta/deviceManagement/virtualEndpoint/userSettings/
PS>TerminatingError(Invoke-MgGraphRequest): “GET https://graph.microsoft.com/beta/deviceManagement/deviceEnrollmentConfigurations/
PS>TerminatingError(Invoke-MgGraphRequest): “GET https://graph.microsoft.com/beta/deviceManagement/deviceCategories/
And more.
And then we have this one:
{“error”:{“code”:”UnknownError”,”message”:”Request policy is tomb – stoned or null or not a health script. Provided id: “,”innerError”:{“date”:”2023-02-08T07:30:25″,”request-id”:””,”client-request-id”:””}}}”
PS>TerminatingError(Invoke-MgGraphRequest): “GET https://graph.microsoft.com/beta/Groups/
I realize it could be some config in my tenant, but it would be nice to get some error handlings/logs that can help us know if the backup is consistant, or for example we need to troubleshooting whatever config might block the script.
Thanks! 🙂
Hi,
It definitely sounds permission based.
Can you contact me via the form on the site and we’ll have a look via email.
The transcript when it runs it stored here: $env:TEMP\intune-$date.log
Thanks
Hello, could you give the script a try with the new modules 1.21.0 released a few days ago, I’m getting various errors all over the place, however it does succeed in backing up the configuration to json on github, and the content of the json seems to be as expected for the configuration I exported. Thanks for all the effort!
Hi,
I have just uploaded a new version, can you try that please? It seems to be working my end, but if you have issues, feel free to email me the errors and I’ll have a look for you
Hi Andrew,
Has your script changed or updated?
the script lines you mentioned in these comments don’t match what you say.
I’m a noob to scripting and i’m super confused with reading this article and trying to figure out your script.
To keep it simple, i have the following questions to which i’d like to request clarity:
1. Which part in the script do I need to modify exactly to automate the backups to get stored in Azure Storage? Can you please point out the specific lines in the script and what the changes would be?
2. Is there an existing script of yours that has only Azure storage as the backup & restore platform and not Github repo.?
I thank you in advance for your help & guidance.
Hi,
Yes, the script is regularly updated with new features and bug fixes, in this case fixing some issues with larger settings catalog policies. The lines you need to amend haven’t been changed, but the line numbers will just have moved further down.
The script grabs all policies into a variable which it then adds to a file and uploads to GitHub, this is the part you need to change so it uploads to Azure.
With the restore, you need to remove the GitHub parts and grab your file from Azure storage into a variable called $decodedbackup
I don’t have a version with Azure storage, the GitHub API gives me better control over versioning when using the GUI
If you’re starting out with PowerShell, this book comes well recommended:
“Learn PowerShell in a Month of Lunches”
Hi Andrew,
Wouldn’t more in the script need to be changed/modified?
For example:
-Line 102-120 (Variables) – as this isn’t pointing to Azure
-Line 212 to 215 (Connect to Graph) – addition of the updated permissions
-Line 2309 – this points to Github
-Line 2312 – should this be replaced with the following for the connection & upload to Azure work correctly:
##Connect to AZURE
$azurePassword = ConvertTo-SecureString $clientSecret -AsPlainText -Force
$psCred = New-Object System.Management.Automation.PSCredential($clientID , $azurePassword)
Connect-AzAccount -Credential $psCred -TenantId $tenantid -ServicePrincipal
##Convert Storage Account to lowercase (just in case)
$storageaccount = $storageaccount.ToLower()
##Upload to Azure Blob
$files = “$env:TEMP\IntuneBackup$date”
$context = New-AzStorageContext -StorageAccountName $storageaccount -StorageAccountKey $storagekey
Get-ChildItem -Path $files -File -Recurse | Set-AzStorageBlobContent -Container $storagecontainer -Context $context
And about your comment to ‘add a line to read the azure file into into a variable called $decodedbackup’ – I’m not sure i follow. Could you please help & paste here what you mean ?
Lines 102-120 just won’t get used because you aren’t calling the variables, same with line 2309
The permissions depends on how you are connecting to Azure, if you are using a shared key you just need headers and a webrequest
That azure connection will work, but that’s uploading the IntuneBackup file which won’t currently exist so you’ll need to create that with the JSON content first.
When performing a restore, you need to read the content into a variable called $decodedbackup so the rest of the script can use it
Hi Andrew,
I had earlier setup the backup as per one of your previous article https://andrewstaylor.com/2022/03/28/intune-backups-part-1-intune-environment/
And as you commented and pointed out this one, i’m trying to setup this one.
While setting up the App registration, i was able to find and grant all except the following 2 permissions:
Policy.ReadWrite.All
RoleAssignmentSchedule.ReadWrite.All
i looked through but do not see the above 2 anymore. Have they been renamed in Azure?
Also, this article mentions how to setup the backup to be saved in Github repo. Is there also a way to backup & restore in Azure storage?
If yes, then from which parts in the script am i supposed to enter/used in the Runbook?
Hi,
Yes, looks like it’s had an update, these two should replace it I think:
Policy.ReadWrite.ConditionalAccess
RoleManagement.ReadWrite.Directory
Line 2312 does the copy to GitHub so you could change that to store in an Azure blob
For the restore, comment out lines 2335 to 2356 and add a line to read the azure file into a variable called $decodedbackup
Here is the command I used:
intune-backup-restore-withgui.ps1 -type backup -selected some -reponame AppManifests -ownername [GITHUB USERNAME] -token [CLASSIC TOKEN]
Can you try putting the reponame, ownername and token in speechmarks and see if that works? I’ve just tested it on my environment and it backed up ok
I get this error when I try to run a backup:
Line |
2293 | (Invoke-RestMethod -Uri $uri -Method put -Headers @{‘Authorization’=’ …
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| The format of value ‘Accept: application/json’ is invalid.
Can you check your github details including the token are all correct? That line is where it uploads the json
Hi,
Based on some research it looks like restoring client apps are not yet supported yet, we can restore only assignments? Also the backup is not taking fill app, only some data which would be helpful to get some info about the app.
That’s correct, win32 apps are encrypted so can’t be extracted for backup and restore