<# .SYNOPSIS Installs the Veeam Backup for Microsoft 365 collector as a SYSTEM scheduled task. .DESCRIPTION - Copies Send-Vb365BackupStatus.ps1 to Program Files (only admins can modify it, which matters because it runs as SYSTEM). - Writes the config to ProgramData\BackupDashboard and restricts that folder to SYSTEM and Administrators, since it contains the API key. - Registers a scheduled task that runs every IntervalMinutes, under Windows PowerShell 5.1 (powershell.exe) unless -PwshPath points at PowerShell 7. Safe to re-run to change settings or update the script. Can sit alongside the other collectors. Dashboard company per job: -OrganizationTenants maps Microsoft 365 organization names to companies; jobs of unmapped organizations use -Tenant, or the organization's name if no -Tenant. .EXAMPLE .\Install-Vb365Collector.ps1 -ApiUrl https://backups.example.com -Tenant "Acme Corp" One client per VB365 server. Prompts for the API key. .EXAMPLE .\Install-Vb365Collector.ps1 -ApiUrl https://backups.example.com -OrganizationTenants @{ 'acme.onmicrosoft.com' = 'Acme Corp'; 'bluewater.onmicrosoft.com' = 'Bluewater Dental' } A shared VB365 server protecting several organizations. #> #Requires -RunAsAdministrator [CmdletBinding()] param( [Parameter(Mandatory)][string]$ApiUrl, [string]$Tenant, [hashtable]$OrganizationTenants = @{}, [string]$SourceSystem = 'Veeam Backup for M365', [ValidateRange(5, 1440)][int]$IntervalMinutes = 60, [ValidateRange(1, 720)][int]$LookbackHours = 48, [string]$InstallDir = (Join-Path $env:ProgramFiles 'BackupDashboard'), # Run under PowerShell 7 instead of Windows PowerShell, e.g. 'C:\Program Files\PowerShell\7\pwsh.exe' [string]$PwshPath ) $ErrorActionPreference = 'Stop' $TaskName = 'Backup Dashboard - VB365 collector' $DataDir = Join-Path $env:ProgramData 'BackupDashboard' $ConfigPath = Join-Path $DataDir 'vb365-collector.json' $Shell = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe' if ($PwshPath) { if (-not (Test-Path $PwshPath)) { throw "PowerShell 7 not found at '$PwshPath'." } $Shell = $PwshPath } # Check VB365 is here before changing anything $moduleFile = Join-Path $env:ProgramFiles 'Veeam\Backup365\Veeam.Archiver.PowerShell\Veeam.Archiver.PowerShell.psd1' if (-not (Get-Module -ListAvailable -Name Veeam.Archiver.PowerShell) -and -not (Test-Path $moduleFile)) { throw 'The Veeam Backup for Microsoft 365 PowerShell module (Veeam.Archiver.PowerShell) is not installed on this server.' } if (-not $Tenant -and $OrganizationTenants.Count -eq 0) { Write-Warning 'No -Tenant or -OrganizationTenants: jobs will be listed under their Microsoft 365 organization name.' } if ($ApiUrl -notmatch '^https://') { Write-Warning 'ApiUrl is not HTTPS: the API key would be sent in clear text.' } # Read the key as a SecureString so it isn't echoed to the console or kept in shell history $secure = Read-Host -AsSecureString 'API key (from: python manage.py create-key )' $bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($secure) try { $apiKey = [Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr) } finally { [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) } if (-not $apiKey.StartsWith('bdk_')) { throw "That doesn't look like a dashboard API key (expected 'bdk_...')." } # Config folder: SYSTEM + Administrators only (SIDs so this works on non-English Windows) New-Item -ItemType Directory -Path $DataDir -Force | Out-Null & icacls.exe $DataDir /inheritance:r /grant:r '*S-1-5-18:(OI)(CI)F' '*S-1-5-32-544:(OI)(CI)F' | Out-Null if ($LASTEXITCODE) { throw "icacls failed on $DataDir" } $config = [ordered]@{ ApiUrl = $ApiUrl.TrimEnd('/') ApiKey = $apiKey SourceSystem = $SourceSystem LookbackHours = $LookbackHours OrganizationTenants = $OrganizationTenants } if ($Tenant) { $config.Tenant = $Tenant } $config | ConvertTo-Json | Set-Content -Path $ConfigPath -Encoding UTF8 New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null $scriptPath = Join-Path $InstallDir 'Send-Vb365BackupStatus.ps1' Copy-Item -Path (Join-Path $PSScriptRoot 'Send-Vb365BackupStatus.ps1') -Destination $scriptPath -Force Unblock-File -Path $scriptPath $action = New-ScheduledTaskAction -Execute $Shell ` -Argument "-NoProfile -NonInteractive -ExecutionPolicy Bypass -File `"$scriptPath`"" $trigger = New-ScheduledTaskTrigger -Once -At (Get-Date).AddMinutes(1) ` -RepetitionInterval (New-TimeSpan -Minutes $IntervalMinutes) $principal = New-ScheduledTaskPrincipal -UserId 'S-1-5-18' -LogonType ServiceAccount -RunLevel Highest $settings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Minutes 30) ` -MultipleInstances IgnoreNew -StartWhenAvailable Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger -Principal $principal ` -Settings $settings -Description 'Sends Veeam Backup for Microsoft 365 results to the Backup Dashboard' -Force | Out-Null Write-Host "Installed. Config: $ConfigPath" Write-Host "Task '$TaskName' runs every $IntervalMinutes minutes as SYSTEM, using $Shell." Write-Host '' Write-Host 'Next steps:' Write-Host " 1. Preview what will be sent: & '$Shell' -File '$scriptPath' -DryRun" Write-Host " 2. Backfill 30 days of history: & '$Shell' -File '$scriptPath' -LookbackHours 720" Write-Host " 3. Check the log: Get-Content '$DataDir\vb365-collector.log' -Tail 20"