- 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 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
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
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
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,
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”
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
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
Thanks for improving it further, Iām using in on my lab. Made some modifications to use azure storage.
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
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,
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
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
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
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
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, 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.