### Before you start: ### 1. Automation account should have a managed identity assigned ### 2. This managed identity has to be added to the designated subscription (todo: maybe just RG would work?) Param ( [Parameter (Mandatory=$true)] [String] $SubscriptionId ) # Terminate runbook on non-handled exception $ErrorActionPreference = "Stop" if (![System.Guid]::TryParse($SubscriptionId, [System.Management.Automation.PSReference][System.Guid]::empty)) { Write-Error "Provided subscription ID is not a valid GUID, exiting" exit 1 } # Ensures you do not inherit an AzContext in your runbook Disable-AzContextAutosave -Scope Process | Out-Null # Connect using a Managed Service Identity try { Connect-AzAccount -Identity | Out-Null Set-AzContext -Subscription $SubscriptionId } catch { Write-Output "There is no system-assigned user identity or requested subscription IAM is misconfigured. Aborting."; Write-Error $Error[0] exit 1 } $compliantVMs = 0 $nonCompliantVMs = 0 $notApplicableVMs = 0 $metadata = @{} $TempFile = New-TemporaryFile $TempFilePath = $TempFile.FullName $ScriptToRun = @" /usr/bin/ubuntu-advantage security-status --format json 2>/dev/null || /usr/bin/ubuntu-advantage security-status --format json --beta "@ $ScriptToRun | Out-File -FilePath $TempFilePath Foreach ($vm in Get-AzVm -status) { if ($null -eq $vm.OsProfile.LinuxConfiguration) { # Not a Linux VM $notApplicableVMs++ Write-Warning "VM $($vm.Id) is not a Linux VM." continue } if ($vm.PowerState -ne 'VM running') { # VM is shut down or not available $notApplicableVMs++ Write-Warning "VM $($vm.Id) is not Running; got status $($vm.PowerState)" continue } $job = Invoke-AzVMRunCommand -AsJob -ResourceGroupName $vm.ResourceGroupName -Name $vm.Name -CommandId RunShellScript -ScriptPath $TempFilePath $metadata[$job.Id] = @{ "MachineId" = $vm.Id; "Job" = $job } } if ($metadata.Keys) { Write-Output "Waiting for machines to finish executing..." Get-Job | Wait-Job | Out-Null } else { Write-Output "No eligible Azure VMs detected. Exiting." exit 0 } foreach($job in $metadata.GetEnumerator()) { $machineId = $job.Value["MachineId"] try { $jobOutput = $job.Value["Job"].Output.Value.Message $stderr = $jobOutput.Split("[stderr]")[1].Trim() if ($stderr.Length -gt 0) { throw $stderr } $stdout = [regex]::match($jobOutput, "(?si)\[stdout\]\n(.*)(?:\n\[stderr\]\n?)") if (!$stdout.Success) { throw $stderr } $jobResult = $stdout.Groups[1].Value.Trim() | ConvertFrom-Json } catch { $nonCompliantVMs++ $err = $($job.Value["Job"].Output.Value.Message) -replace '\r*\n', ' ' Write-Error "VM $($machineId) error parsing ua output: $($err)" continue } $securityUpdates = $jobResult.summary.num_standard_security_updates $esmInfraUpdates = $jobResult.summary.num_esm_infra_updates $esmAppsUpdates = $jobResult.summary.num_esm_apps_updates $totalOutdatedPackages = $securityUpdates + $esmAppsUpdates + $esmInfraUpdates if ($totalOutdatedPackages > 0) { Write-Output "machine $($machineId) has $($totalOutdatedPackages) packages outdated" $nonCompliantVMs++ } else { $compliantVMs++ } } $prettifiedOutput = @" Total VMs: $($compliantVMs + $nonCompliantVMs + $notApplicableVMs) Compliant VMs: $($compliantVMs) Non-compliant VMs: $($nonCompliantVMs) N/A: $($notApplicableVMs) "@ Write-Output $prettifiedOutput