If you’ve used NinjaOne RMM for any length of time, you’ve probably accumulated a large number of mission critical custom Powershell scripts and automations. If you’ve ever looked into source control or backing up of these scripts, you’ve probably discovered that no solutions currently exist. The closest you have is to set up something like Github Enterprise, host your scripts there and then set up stub scripts inside of Ninja. This of course adds another layer of complexity and more importantly another point of failure. Until such time as NinjaOne implements a proper source control solution, we’re left to fend for ourselves.
If nothing else, I want to be able to download all of our scripts on a regular basis for backups, a poor mans source control and to ensure we retain access to our intellectual property.
I sat down today to try and find a way to automate backing up the scripts and I’m pleased to report I found a solution.
There is an API endpoint for scripts but unfortunately it only tells you the NAME of script, not the body. My workaround to this in the script below is to use the API to generate a list of all the scripts in our environment and their associated index IDs. Then use invoke-webrequest to pretend to be a human and connect to one script URL after the other and copy the script contents. To do this requires a valid and current session key. You can grab this from the Dev tools of your favorite browser after signing in to Ninja.
The script below is rough around the edges as I just made it for me to solve an immediate problem. But it does work so feel free to build on it as required.
The script will download all scripts and meta data and save them into a dated folder. It will keep 10 folders and delete any older backups by default.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 |
<# To use this script: 1) Create an API user in your NinjaOne Admin control panel here: https://XX.ninjarmm.com/#/administration/apps/api 2) Update this script to replace xx.ninjarmm.com with the region where your instance is located 3) Log into Ninja normally in a browser using your regular admin credentials that have access to all scripts 4) Browse to any script, eg https://XX.ninjarmm.com/#/editor/script/123 5) Open Dev tools (F12) 6) Go to the APPLICATION tab in the Dev tools panel, go to the APPLICATION tab and then click on COOKIES. Expand the ninja site and you should see a SESSION KEY. COPY that key 7) Paste in the session key when prompted #> Function Invoke-NinjaAPI($Method, $ClientID, $ClientSecret) { $AuthBody = @{ 'grant_type' = 'client_credentials' 'client_id' = $ClientID 'client_secret' = $ClientSecret 'scope' = 'monitoring' } $Result = invoke-webrequest -Uri "$($NinjaURL)/ws/oauth/token" -Method POST -Body $AuthBody -ContentType 'application/x-www-form-urlencoded' -UseBasicParsing $AuthHeader = @{'Authorization' = "Bearer $(($Result.content | ConvertFrom-Json).access_token)"} (Invoke-WebRequest -Uri "$($NinjaURL)/api/v2/$method" -Method GET -Headers $AuthHeader -ContentType 'application/json' -UseBasicParsing).content | convertfrom-json } ######################## ### MAIN CODE START #### ######################## $NinjaURL = "https://ca.ninjarmm.com" $NinjaURLAuthority = "ca.ninjarmm.com" $TargetRootFolder = "c:\temp\n1" $ClientID = Read-Host "(1 of 3) Enter your client ID (setup here https://XX.ninjarmm.com/#/administration/apps/api)" $ClientSecretSecure = Read-Host "(2 of 3) Enter your Client Secret" -AsSecureString $ClientSecret = ConvertFrom-SecureString $ClientSecretSecure -AsPlainText $sessionKeySecure = Read-Host "(3 of 3) Enter your session key (see comments in script more info)" -AsSecureString $SessionKey = ConvertFrom-SecureString $SessionKeySecure -AsPlainText # Get a list of scripts and their IDs from the API that so we know what URLs to query directly to download the scripts themselves $ScriptsList = Invoke-NinjaAPI -Method "automation/scripts" -ClientID $ClientID -ClientSecret $ClientSecret $ScriptData = $scriptslist | where {$_.language -ne 'native'} | select id, name, language $ScriptCount = ($ScriptData | Measure-object).count $i = 1; $FolderDate = Get-Date -Format yyyy-MM-dd-HHmm New-Item -ItemType Directory -Force -Path $TargetRootFolder\$FolderDate | Out-Null $TargetPath = "$TargetRootFolder\$FolderDate" $scriptdata | select -first 1 ForEach($Row in $ScriptData) { $ts = [string](Get-Date) write-host "[$ts] Processing $i of $ScriptCount..." $session = New-Object Microsoft.PowerShell.Commands.WebRequestSession $session.UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36" $session.Cookies.Add((New-Object System.Net.Cookie("sessionKey", "$SessionKey", "/", "$NinjaURLAuthority"))) $PageContents = Invoke-WebRequest -UseBasicParsing -Uri "$NinjaURL/swb/s2/scripting/scripts/$($Row.id)" ` -WebSession $session ` -Headers @{ "authority"="$NINJAURLAuthority" "method"="GET" "path"="/swb/s2/scripting/scripts/$($Row.id)" "scheme"="https" "accept"="*/*" "accept-encoding"="gzip, deflate, br, zstd" "accept-language"="en-US,en;q=0.9" "cache-control"="no-cache" "pragma"="no-cache" "priority"="u=1, i" "referer"="$NinjaURL" "sec-ch-ua"="`"Not;A=Brand`";v=`"8`", `"Chromium`";v=`"150`", `"Google Chrome`";v=`"150`"" "sec-ch-ua-arch"="`"x86`"" "sec-ch-ua-mobile"="?0" "sec-ch-ua-platform"="`"Windows`"" "sec-fetch-dest"="empty" "sec-fetch-mode"="cors" "sec-fetch-site"="same-origin" } ` -ContentType "application/json" $Code = ($PageContents.content | convertfrom-json).code $b64 = $code -replace '\s','' # Remove line breaks and spaces $script = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($b64)) $FileExt = switch($Row.language.trim()) { 'powershell' { '.ps1' } 'batchfile' { '.bat'} 'sh' { '.sh'} default { 'UNKNOWN_EXTENSION'} } $FileName = $Row.name -replace '[^A-Za-z0-9]', '_' $FileName = [string]$row.id + "_" + $Filename + $FileExt $script | out-file "$TargetPath\$Filename" -Verbose $waittime = Get-Random -Minimum 100 -Maximum 2000 start-sleep -Milliseconds $waittime $i++ } # Save table of contents file with the notes and last updated metadata from each script $ScriptsList | out-file $TargetPath\#TableofContents.txt # Remove older files # Get folders matching the timestamp naming pattern $folders = Get-ChildItem -Path $TargetRootFolder -Directory | Where { $_.Name -match '^\d{4}-\d{2}-\d{2}-\d{4}$' } | Sort-Object Name -Descending # Keep the newest 10, delete older ones $foldersToDelete = $folders | Select-Object -Skip 10 ForEach ($folder in $foldersToDelete) { Write-Host "Deleting $($folder.FullName)" Remove-Item -Path $folder.FullName -Recurse -Force } |